feat: Implement comprehensive non-hierarchical logging system

- Created new logging infrastructure with per-component filtering
- Added 6 log levels: DEBUG, INFO, API, WARNING, ERROR, CRITICAL
- Implemented non-hierarchical level control (any combination can be enabled)
- Migrated 917 print() statements across 31 files to structured logging
- Created web UI (system.html) for runtime configuration with dark theme
- Added global level controls to enable/disable levels across all components
- Added timestamp format control (off/time/date/datetime options)
- Implemented log rotation (10MB per file, 5 backups)
- Added API endpoints for dynamic log configuration
- Configured HTTP request logging with filtering via api.requests component
- Intercepted APScheduler logs with proper formatting
- Fixed persistence paths to use /app/memory for Docker volume compatibility
- Fixed checkbox display bug in web UI (enabled_levels now properly shown)
- Changed System Settings button to open in same tab instead of new window

Components: bot, api, api.requests, autonomous, persona, vision, llm,
conversation, mood, dm, scheduled, gpu, media, server, commands,
sentiment, core, apscheduler

All settings persist across container restarts via JSON config.
This commit is contained in:
2026-01-10 20:46:19 +02:00
parent ce00f9bd95
commit 32c2a7b930
34 changed files with 2766 additions and 936 deletions

View File

@@ -50,8 +50,25 @@ from utils.figurine_notifier import (
send_figurine_dm_to_single_user send_figurine_dm_to_single_user
) )
from utils.dm_logger import dm_logger 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() nest_asyncio.apply()
# Initialize API logger
logger = get_logger('api')
api_requests_logger = get_logger('api.requests')
# ========== GPU Selection Helper ========== # ========== GPU Selection Helper ==========
def get_current_gpu_url(): def get_current_gpu_url():
"""Get the URL for the currently selected GPU""" """Get the URL for the currently selected GPU"""
@@ -70,6 +87,58 @@ def get_current_gpu_url():
app = FastAPI() 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 # Serve static folder
app.mount("/static", StaticFiles(directory="static"), name="static") 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 channel_id, message_id, globals.client, data.context
) )
if not success: 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()) globals.client.loop.create_task(trigger_from_message())
@@ -419,17 +488,17 @@ def trigger_dialogue(data: dict):
continue continue
if not message: if not message:
print(f"⚠️ Message {message_id} not found") logger.error(f"Message {message_id} not found")
return return
# Check if there's already an argument or dialogue in progress # Check if there's already an argument or dialogue in progress
dialogue_manager = get_dialogue_manager() dialogue_manager = get_dialogue_manager()
if dialogue_manager.is_dialogue_active(message.channel.id): 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 return
if is_argument_in_progress(message.channel.id): 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 return
# Determine current persona from the message author # 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" current_persona = "evil" if globals.EVIL_MODE else "miku"
else: else:
# User message - can't trigger dialogue from user messages # 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 return
opposite_persona = "evil" if current_persona == "miku" else "miku" 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) # Force start the dialogue (bypass interjection check)
dialogue_manager.start_dialogue(message.channel.id) dialogue_manager.start_dialogue(message.channel.id)
@@ -459,7 +528,7 @@ def trigger_dialogue(data: dict):
) )
except Exception as e: except Exception as e:
print(f"⚠️ Error triggering dialogue: {e}") logger.error(f"Error triggering dialogue: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
@@ -514,8 +583,6 @@ def get_gpu_status():
@app.post("/gpu-select") @app.post("/gpu-select")
async def select_gpu(request: Request): async def select_gpu(request: Request):
"""Select which GPU to use for inference""" """Select which GPU to use for inference"""
from utils.gpu_preload import preload_amd_models
data = await request.json() data = await request.json()
gpu = data.get("gpu", "nvidia").lower() gpu = data.get("gpu", "nvidia").lower()
@@ -532,16 +599,10 @@ async def select_gpu(request: Request):
with open(gpu_state_file, "w") as f: with open(gpu_state_file, "w") as f:
json.dump(state, f, indent=2) json.dump(state, f, indent=2)
print(f"🎮 GPU Selection: Switched to {gpu.upper()} GPU") logger.info(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...")
return {"status": "ok", "message": f"Switched to {gpu.upper()} GPU", "gpu": gpu} return {"status": "ok", "message": f"Switched to {gpu.upper()} GPU", "gpu": gpu}
except Exception as e: except Exception as e:
print(f"🎮 GPU Selection Error: {e}") logger.error(f"GPU Selection Error: {e}")
return {"status": "error", "message": str(e)} return {"status": "error", "message": str(e)}
@app.get("/bipolar-mode/arguments") @app.get("/bipolar-mode/arguments")
@@ -574,17 +635,17 @@ async def set_server_mood_endpoint(guild_id: int, data: MoodSetRequest):
# Check if server exists # Check if server exists
if guild_id not in server_manager.servers: 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"} return {"status": "error", "message": "Server not found"}
# Check if mood is valid # Check if mood is valid
from utils.moods import MOOD_EMOJIS from utils.moods import MOOD_EMOJIS
if data.mood not in 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())}"} 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) 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: if success:
# V2: Notify autonomous engine of mood change # 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 from utils.autonomous import on_mood_change
on_mood_change(guild_id, data.mood) on_mood_change(guild_id, data.mood)
except Exception as e: 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 # Update the nickname for this server
from utils.moods import update_server_nickname 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)) globals.client.loop.create_task(update_server_nickname(guild_id))
return {"status": "ok", "new_mood": data.mood, "guild_id": 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"} return {"status": "error", "message": "Failed to set server mood"}
@app.post("/servers/{guild_id}/mood/reset") @app.post("/servers/{guild_id}/mood/reset")
async def reset_server_mood_endpoint(guild_id: int): async def reset_server_mood_endpoint(guild_id: int):
"""Reset mood to neutral for a specific server""" """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 # Check if server exists
if guild_id not in server_manager.servers: 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"} 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") 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: if success:
# V2: Notify autonomous engine of mood change # 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 from utils.autonomous import on_mood_change
on_mood_change(guild_id, "neutral") on_mood_change(guild_id, "neutral")
except Exception as e: 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 # Update the nickname for this server
from utils.moods import update_server_nickname 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)) globals.client.loop.create_task(update_server_nickname(guild_id))
return {"status": "ok", "new_mood": "neutral", "guild_id": 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"} return {"status": "error", "message": "Failed to reset server mood"}
@app.get("/servers/{guild_id}/mood/state") @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): 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 guild_id is provided, detect and join conversation only for that server
# If no guild_id, trigger for all servers # 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 globals.client and globals.client.loop and globals.client.loop.is_running():
if guild_id is not None: if guild_id is not None:
# Trigger for specific server only (force=True to bypass checks when manually triggered) # 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 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)) 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}"} return {"status": "ok", "message": f"Detect and join conversation queued for server {guild_id}"}
else: else:
# Trigger for all servers (force=True to bypass checks when manually triggered) # 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 from utils.autonomous import miku_detect_and_join_conversation
globals.client.loop.create_task(miku_detect_and_join_conversation(force=True)) globals.client.loop.create_task(miku_detect_and_join_conversation(force=True))
return {"status": "ok", "message": "Detect and join conversation queued for all servers"} return {"status": "ok", "message": "Detect and join conversation queued for all servers"}
else: 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"} return {"status": "error", "message": "Bot not ready"}
@app.post("/profile-picture/change") @app.post("/profile-picture/change")
@@ -834,7 +895,7 @@ async def trigger_profile_picture_change(
custom_image_bytes = None custom_image_bytes = None
if file: if file:
custom_image_bytes = await file.read() 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 # Change profile picture
result = await profile_picture_manager.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: except Exception as e:
print(f"⚠️ Error in profile picture API: {e}") logger.error(f"Error in profile picture API: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return {"status": "error", "message": f"Unexpected error: {str(e)}"} return {"status": "error", "message": f"Unexpected error: {str(e)}"}
@@ -955,7 +1016,7 @@ async def manual_send(
'content': file_content 'content': file_content
}) })
except Exception as e: 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}"} return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}
# Use create_task to avoid timeout context manager error # Use create_task to avoid timeout context manager error
@@ -967,28 +1028,28 @@ async def manual_send(
try: try:
reference_message = await channel.fetch_message(int(reply_to_message_id)) reference_message = await channel.fetch_message(int(reply_to_message_id))
except Exception as e: 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 return
# Send the main message # Send the main message
if message.strip(): if message.strip():
if reference_message: if reference_message:
await channel.send(message, reference=reference_message, mention_author=mention_author) 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: else:
await channel.send(message) 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 # Send files if any
for file_info in file_data: for file_info in file_data:
try: try:
await channel.send(file=discord.File(io.BytesIO(file_info['content']), filename=file_info['filename'])) 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: 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: 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()) globals.client.loop.create_task(send_message_and_files())
return {"status": "ok", "message": "Message and files queued for sending"} return {"status": "ok", "message": "Message and files queued for sending"}
@@ -1028,7 +1089,7 @@ async def manual_send_webhook(
'content': file_content 'content': file_content
}) })
except Exception as e: 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}"} return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}
# Use create_task to avoid timeout context manager error # 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) # Get or create webhooks for this channel (inside the task)
webhooks = await get_or_create_webhooks_for_channel(channel) webhooks = await get_or_create_webhooks_for_channel(channel)
if not webhooks: 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 return
# Select the appropriate webhook # Select the appropriate webhook
@@ -1065,10 +1126,10 @@ async def manual_send_webhook(
) )
persona_name = "Evil Miku" if persona == "evil" else "Hatsune Miku" 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: except Exception as e:
print(f"Failed to send webhook message: {e}") logger.error(f"Failed to send webhook message: {e}")
import traceback import traceback
traceback.print_exc() 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)): async def figurines_send_now(tweet_url: str = Form(None)):
"""Trigger immediate figurine DM send to all subscribers, optionally with specific tweet URL""" """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(): 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)) 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": "ok", "message": "Figurine DMs queued"}
return {"status": "error", "message": "Bot not ready"} 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") @app.post("/figurines/send_to_user")
async def figurines_send_to_user(user_id: str = Form(...), tweet_url: str = Form(None)): 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""" """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(): 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"} return {"status": "error", "message": "Bot not ready"}
try: try:
user_id_int = int(user_id) 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: 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"} return {"status": "error", "message": "Invalid user ID"}
# Clean up tweet URL if it's empty string # Clean up tweet URL if it's empty string
if tweet_url == "": if tweet_url == "":
tweet_url = None 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 # 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)) 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") @app.get("/servers")
def get_servers(): def get_servers():
"""Get all configured servers""" """Get all configured servers"""
print(f"🎭 API: /servers endpoint called") logger.debug("/servers endpoint called")
print(f"🎭 API: server_manager.servers keys: {list(server_manager.servers.keys())}") logger.debug(f"server_manager.servers keys: {list(server_manager.servers.keys())}")
print(f"🎭 API: server_manager.servers count: {len(server_manager.servers)}") logger.debug(f"server_manager.servers count: {len(server_manager.servers)}")
# Debug: Check config file directly # Debug: Check config file directly
config_file = server_manager.config_file 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): if os.path.exists(config_file):
try: try:
with open(config_file, "r", encoding="utf-8") as f: with open(config_file, "r", encoding="utf-8") as f:
config_data = json.load(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: except Exception as e:
print(f"🎭 API: Failed to read config file: {e}") logger.error(f"Failed to read config file: {e}")
else: else:
print(f"🎭 API: Config file does not exist") logger.warning("Config file does not exist")
servers = [] servers = []
for server in server_manager.get_all_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']) server_data['guild_id'] = str(server_data['guild_id'])
servers.append(server_data) servers.append(server_data)
print(f"🎭 API: Adding server to response: {server_data['guild_id']} - {server_data['guild_name']}") logger.debug(f"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"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 # Debug: Show exact JSON being sent
import json import json
@@ -1316,7 +1377,7 @@ def update_server(guild_id: int, data: dict):
@app.post("/servers/{guild_id}/bedtime-range") @app.post("/servers/{guild_id}/bedtime-range")
def update_server_bedtime_range(guild_id: int, data: dict): def update_server_bedtime_range(guild_id: int, data: dict):
"""Update server bedtime range configuration""" """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 # Validate the data
required_fields = ['bedtime_hour', 'bedtime_minute', 'bedtime_hour_end', 'bedtime_minute_end'] 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) # Update just the bedtime job for this server (avoid restarting all schedulers)
job_success = server_manager.update_server_bedtime_job(guild_id, globals.client) job_success = server_manager.update_server_bedtime_job(guild_id, globals.client)
if job_success: if job_success:
print(f"✅ API: Bedtime range updated for server {guild_id}") logger.info(f"Bedtime range updated for server {guild_id}")
return { return {
"status": "ok", "status": "ok",
"message": f"Bedtime range updated: {bedtime_hour:02d}:{bedtime_minute:02d} - {bedtime_hour_end:02d}:{bedtime_minute_end:02d}" "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: try:
response = await query_llama(req.prompt, user_id=user_id, guild_id=None, response_type="dm_response") response = await query_llama(req.prompt, user_id=user_id, guild_id=None, response_type="dm_response")
await user.send(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 # Log to DM history
from utils.dm_logger import dm_logger from utils.dm_logger import dm_logger
dm_logger.log_conversation(user_id, req.prompt, response) dm_logger.log_conversation(user_id, req.prompt, response)
except Exception as e: 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 # Use create_task to avoid timeout context manager error
globals.client.loop.create_task(send_dm_custom_prompt()) globals.client.loop.create_task(send_dm_custom_prompt())
@@ -1456,7 +1517,7 @@ async def send_manual_message_dm(
'content': file_content 'content': file_content
}) })
except Exception as e: 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}"} return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}
async def send_dm_message_and_files(): 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() dm_channel = user.dm_channel or await user.create_dm()
reference_message = await dm_channel.fetch_message(int(reply_to_message_id)) reference_message = await dm_channel.fetch_message(int(reply_to_message_id))
except Exception as e: 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 return
# Send the main message # Send the main message
if message.strip(): if message.strip():
if reference_message: if reference_message:
await user.send(message, reference=reference_message, mention_author=mention_author) 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: else:
await user.send(message) 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 # Send files if any
for file_info in file_data: for file_info in file_data:
try: try:
await user.send(file=discord.File(io.BytesIO(file_info['content']), filename=file_info['filename'])) 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: 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) # Log to DM history (user message = manual override trigger, miku response = the message sent)
from utils.dm_logger import dm_logger 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]) dm_logger.log_conversation(user_id, "[Manual Override Trigger]", message, attachments=[f['filename'] for f in file_data])
except Exception as e: 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 # Use create_task to avoid timeout context manager error
globals.client.loop.create_task(send_dm_message_and_files()) 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): async def view_generated_image(filename: str):
"""Serve generated images from ComfyUI output directory""" """Serve generated images from ComfyUI output directory"""
try: try:
print(f"🖼️ Image view request for: {filename}") logger.debug(f"Image view request for: {filename}")
# Try multiple possible paths for ComfyUI output # Try multiple possible paths for ComfyUI output
possible_paths = [ possible_paths = [
@@ -1572,13 +1633,13 @@ async def view_generated_image(filename: str):
for path in possible_paths: for path in possible_paths:
if os.path.exists(path): if os.path.exists(path):
image_path = path image_path = path
print(f"Found image at: {path}") logger.debug(f"Found image at: {path}")
break break
else: else:
print(f"Not found at: {path}") logger.debug(f"Not found at: {path}")
if not image_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}"} return {"status": "error", "message": f"Image not found: {filename}"}
# Determine content type based on file extension # Determine content type based on file extension
@@ -1591,11 +1652,11 @@ async def view_generated_image(filename: str):
elif ext == "webp": elif ext == "webp":
content_type = "image/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) return FileResponse(image_path, media_type=content_type)
except Exception as e: 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}"} return {"status": "error", "message": f"Error serving image: {e}"}
@app.post("/servers/{guild_id}/autonomous/tweet") @app.post("/servers/{guild_id}/autonomous/tweet")
@@ -1638,36 +1699,36 @@ def get_available_moods():
@app.post("/test/mood/{guild_id}") @app.post("/test/mood/{guild_id}")
async def test_mood_change(guild_id: int, data: MoodSetRequest): async def test_mood_change(guild_id: int, data: MoodSetRequest):
"""Test endpoint for debugging mood changes""" """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 # Check if server exists
if guild_id not in server_manager.servers: if guild_id not in server_manager.servers:
return {"status": "error", "message": f"Server {guild_id} not found"} return {"status": "error", "message": f"Server {guild_id} not found"}
server_config = server_manager.get_server_config(guild_id) 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 # Try to set mood
success = server_manager.set_server_mood(guild_id, data.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: if success:
# V2: Notify autonomous engine of mood change # V2: Notify autonomous engine of mood change
try: try:
from utils.autonomous import on_mood_change from utils.autonomous import on_mood_change
on_mood_change(guild_id, data.mood) 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: 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 # Try to update nickname
from utils.moods import update_server_nickname from utils.moods import update_server_nickname
print(f"🧪 TEST: Attempting nickname update...") logger.debug(f"TEST: Attempting nickname update...")
try: try:
await update_server_nickname(guild_id) await update_server_nickname(guild_id)
print(f"🧪 TEST: Nickname update completed") logger.debug(f"TEST: Nickname update completed")
except Exception as e: except Exception as e:
print(f"🧪 TEST: Nickname update failed: {e}") logger.error(f"TEST: Nickname update failed: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
@@ -1707,10 +1768,10 @@ def get_dm_conversations(user_id: str, limit: int = 50):
from utils.dm_logger import dm_logger from utils.dm_logger import dm_logger
# Convert string user_id to int for internal processing # Convert string user_id to int for internal processing
user_id_int = int(user_id) 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) 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"] 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: if "message_id" in conv:
conv["message_id"] = str(conv["message_id"]) 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 # Debug: Show message IDs being returned
for i, conv in enumerate(conversations): for i, conv in enumerate(conversations):
msg_id = conv.get("message_id", "") msg_id = conv.get("message_id", "")
is_bot = conv.get("is_bot_message", False) is_bot = conv.get("is_bot_message", False)
content_preview = conv.get("content", "")[:30] + "..." if conv.get("content", "") else "[No content]" 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} return {"status": "ok", "conversations": conversations}
except ValueError: except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"} return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e: 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}"} return {"status": "error", "message": f"Failed to get conversations: {e}"}
@app.get("/dms/users/{user_id}/search") @app.get("/dms/users/{user_id}/search")
@@ -1792,7 +1853,7 @@ def get_blocked_users():
blocked_users = dm_logger.get_blocked_users() blocked_users = dm_logger.get_blocked_users()
return {"status": "ok", "blocked_users": blocked_users} return {"status": "ok", "blocked_users": blocked_users}
except Exception as e: 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}"} return {"status": "error", "message": f"Failed to get blocked users: {e}"}
@app.post("/dms/users/{user_id}/block") @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) success = dm_logger.block_user(user_id_int, username)
if success: 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"} return {"status": "ok", "message": f"User {username} has been blocked"}
else: else:
return {"status": "error", "message": f"User {username} is already blocked"} return {"status": "error", "message": f"User {username} is already blocked"}
@@ -1816,7 +1877,7 @@ def block_user(user_id: str):
except ValueError: except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"} return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e: 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}"} return {"status": "error", "message": f"Failed to block user: {e}"}
@app.post("/dms/users/{user_id}/unblock") @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) success = dm_logger.unblock_user(user_id_int)
if success: 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"} return {"status": "ok", "message": f"User has been unblocked"}
else: else:
return {"status": "error", "message": f"User is not blocked"} return {"status": "error", "message": f"User is not blocked"}
@@ -1835,7 +1896,7 @@ def unblock_user(user_id: str):
except ValueError: except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"} return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e: 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}"} return {"status": "error", "message": f"Failed to unblock user: {e}"}
@app.post("/dms/users/{user_id}/conversations/{conversation_id}/delete") @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 # For now, return success immediately since we can't await in FastAPI sync endpoint
# The actual deletion happens asynchronously # 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)"} return {"status": "ok", "message": "Message deletion queued (will delete from both Discord and logs)"}
except ValueError: except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"} return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e: 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}"} return {"status": "error", "message": f"Failed to delete conversation: {e}"}
@app.post("/dms/users/{user_id}/conversations/delete-all") @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()) success = globals.client.loop.create_task(do_delete_all())
# Return success immediately since we can't await in FastAPI sync endpoint # 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)"} return {"status": "ok", "message": "Bulk deletion queued (will delete all Miku messages from Discord and clear logs)"}
except ValueError: except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"} return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e: 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}"} return {"status": "error", "message": f"Failed to delete conversations: {e}"}
@app.post("/dms/users/{user_id}/delete-completely") @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) success = dm_logger.delete_user_completely(user_id_int)
if success: 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"} return {"status": "ok", "message": "User data deleted completely"}
else: else:
return {"status": "error", "message": "No user data found"} return {"status": "error", "message": "No user data found"}
@@ -1901,7 +1962,7 @@ def delete_user_completely(user_id: str):
except ValueError: except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"} return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e: 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}"} return {"status": "error", "message": f"Failed to delete user: {e}"}
# ========== DM Interaction Analysis Endpoints ========== # ========== DM Interaction Analysis Endpoints ==========
@@ -1923,7 +1984,7 @@ def run_dm_analysis():
return {"status": "ok", "message": "DM analysis started"} return {"status": "ok", "message": "DM analysis started"}
except Exception as e: 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}"} return {"status": "error", "message": f"Failed to run DM analysis: {e}"}
@app.post("/dms/users/{user_id}/analyze") @app.post("/dms/users/{user_id}/analyze")
@@ -1949,7 +2010,7 @@ def analyze_user_interaction(user_id: str):
except ValueError: except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"} return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e: 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}"} return {"status": "error", "message": f"Failed to analyze user: {e}"}
@app.get("/dms/analysis/reports") @app.get("/dms/analysis/reports")
@@ -1974,11 +2035,11 @@ def get_analysis_reports(limit: int = 20):
report['filename'] = filename report['filename'] = filename
reports.append(report) reports.append(report)
except Exception as e: 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} return {"status": "ok", "reports": reports}
except Exception as e: 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}"} return {"status": "error", "message": f"Failed to get reports: {e}"}
@app.get("/dms/analysis/reports/{user_id}") @app.get("/dms/analysis/reports/{user_id}")
@@ -2005,13 +2066,13 @@ def get_user_reports(user_id: str, limit: int = 10):
report['filename'] = filename report['filename'] = filename
reports.append(report) reports.append(report)
except Exception as e: 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} return {"status": "ok", "reports": reports}
except ValueError: except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"} return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e: 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}"} return {"status": "error", "message": f"Failed to get user reports: {e}"}
# ========== Message Reaction Endpoint ========== # ========== Message Reaction Endpoint ==========
@@ -2043,15 +2104,15 @@ async def add_reaction_to_message(
try: try:
message = await channel.fetch_message(msg_id) message = await channel.fetch_message(msg_id)
await message.add_reaction(emoji) 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: 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: 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: except discord.HTTPException as e:
print(f"Failed to add reaction: {e}") logger.error(f"Failed to add reaction: {e}")
except Exception as 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()) globals.client.loop.create_task(add_reaction_task())
@@ -2061,7 +2122,7 @@ async def add_reaction_to_message(
} }
except Exception as e: 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}"} return {"status": "error", "message": f"Failed to add reaction: {e}"}
# ========== Autonomous V2 Endpoints ========== # ========== 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: except Exception as e:
error_msg = f"Error in chat stream: {str(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" yield f"data: {json.dumps({'error': error_msg})}\n\n"
return StreamingResponse( 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(): def start_api():
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=3939) uvicorn.run(app, host="0.0.0.0", port=3939)

View File

@@ -46,9 +46,13 @@ from utils.autonomous import (
) )
from utils.dm_logger import dm_logger from utils.dm_logger import dm_logger
from utils.dm_interaction_analyzer import init_dm_analyzer from utils.dm_interaction_analyzer import init_dm_analyzer
from utils.logger import get_logger
import globals import globals
# Initialize bot logger
logger = get_logger('bot')
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format="%(asctime)s %(levelname)s: %(message)s", format="%(asctime)s %(levelname)s: %(message)s",
@@ -61,10 +65,14 @@ logging.basicConfig(
@globals.client.event @globals.client.event
async def on_ready(): async def on_ready():
print(f'🎤 MikuBot connected as {globals.client.user}') logger.info(f'🎤 MikuBot connected as {globals.client.user}')
print(f'💬 DM support enabled - users can message Miku directly!') logger.info(f'💬 DM support enabled - users can message Miku directly!')
globals.BOT_USER = globals.client.user 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) # Restore evil mode state from previous session (if any)
from utils.evil_mode import restore_evil_mode_on_startup from utils.evil_mode import restore_evil_mode_on_startup
@@ -77,7 +85,7 @@ async def on_ready():
# Initialize DM interaction analyzer # Initialize DM interaction analyzer
if globals.OWNER_USER_ID and globals.OWNER_USER_ID != 0: if globals.OWNER_USER_ID and globals.OWNER_USER_ID != 0:
init_dm_analyzer(globals.OWNER_USER_ID) 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) # Schedule daily DM analysis (runs at 2 AM every day)
from utils.scheduled import run_daily_dm_analysis from utils.scheduled import run_daily_dm_analysis
@@ -88,9 +96,9 @@ async def on_ready():
minute=0, minute=0,
id='daily_dm_analysis' id='daily_dm_analysis'
) )
print("⏰ Scheduled daily DM analysis at 2:00 AM") logger.info("⏰ Scheduled daily DM analysis at 2:00 AM")
else: 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 (now handled by server manager)
setup_autonomous_speaking() setup_autonomous_speaking()
@@ -146,7 +154,7 @@ async def on_message(message):
await replied_msg.reply(file=discord.File(output_video)) await replied_msg.reply(file=discord.File(output_video))
except Exception as e: 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.") await message.channel.send("Sorry, something went wrong while generating the video.")
return return
@@ -159,11 +167,11 @@ async def on_message(message):
miku_addressed = await is_miku_addressed(message) miku_addressed = await is_miku_addressed(message)
if is_dm: 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 # Check if user is blocked
if dm_logger.is_user_blocked(message.author.id): 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 return
# Log the user's DM message # Log the user's DM message
@@ -185,7 +193,7 @@ async def on_message(message):
# Add reply context marker to the prompt # Add reply context marker to the prompt
prompt = f'[Replying to your message: "{replied_content}"] {prompt}' prompt = f'[Replying to your message: "{replied_content}"] {prompt}'
except Exception as e: 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(): async with message.channel.typing():
# If message has an image, video, or GIF attachment # If message has an image, video, or GIF attachment
@@ -212,9 +220,9 @@ async def on_message(message):
) )
if is_dm: 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: 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) 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" current_persona = "evil" if globals.EVIL_MODE else "miku"
asyncio.create_task(check_for_interjection(response_message, current_persona)) asyncio.create_task(check_for_interjection(response_message, current_persona))
except Exception as e: except Exception as e:
print(f"⚠️ Error checking for persona interjection: {e}") logger.error(f"Error checking for persona interjection: {e}")
return return
@@ -239,7 +247,7 @@ async def on_message(message):
is_gif = attachment.filename.lower().endswith('.gif') is_gif = attachment.filename.lower().endswith('.gif')
media_type = "gif" if is_gif else "video" 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 # Download the media
media_bytes_b64 = await download_and_encode_media(attachment.url) 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 it's a GIF, convert to MP4 for better processing
if is_gif: 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) mp4_bytes = await convert_gif_to_mp4(media_bytes)
if mp4_bytes: if mp4_bytes:
media_bytes = mp4_bytes media_bytes = mp4_bytes
print(f"✅ GIF converted to MP4") logger.info(f"✅ GIF converted to MP4")
else: else:
print(f"⚠️ GIF conversion failed, trying direct processing") logger.warning(f"GIF conversion failed, trying direct processing")
# Extract frames # Extract frames
frames = await extract_video_frames(media_bytes, num_frames=6) 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!") await message.channel.send(f"I couldn't extract frames from that {media_type}, sorry!")
return 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 # Analyze the video/GIF with appropriate media type
video_description = await analyze_video_with_vision(frames, media_type=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: 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: 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) 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" current_persona = "evil" if globals.EVIL_MODE else "miku"
asyncio.create_task(check_for_interjection(response_message, current_persona)) asyncio.create_task(check_for_interjection(response_message, current_persona))
except Exception as e: except Exception as e:
print(f"⚠️ Error checking for persona interjection: {e}") logger.error(f"Error checking for persona interjection: {e}")
return return
@@ -310,7 +318,7 @@ async def on_message(message):
for embed in message.embeds: for embed in message.embeds:
# Handle Tenor GIF embeds specially (Discord uses these for /gif command) # Handle Tenor GIF embeds specially (Discord uses these for /gif command)
if embed.type == 'gifv' and embed.url and 'tenor.com' in embed.url: 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 # Extract the actual GIF URL from Tenor
gif_url = await extract_tenor_gif_url(embed.url) gif_url = await extract_tenor_gif_url(embed.url)
@@ -322,7 +330,7 @@ async def on_message(message):
gif_url = embed.thumbnail.url gif_url = embed.thumbnail.url
if not gif_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 continue
# Download the GIF # Download the GIF
@@ -336,13 +344,13 @@ async def on_message(message):
media_bytes = base64.b64decode(media_bytes_b64) media_bytes = base64.b64decode(media_bytes_b64)
# Convert GIF to MP4 # 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) mp4_bytes = await convert_gif_to_mp4(media_bytes)
if not mp4_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 mp4_bytes = media_bytes
else: else:
print(f"Tenor GIF converted to MP4") logger.debug(f"Tenor GIF converted to MP4")
# Extract frames # Extract frames
frames = await extract_video_frames(mp4_bytes, num_frames=6) 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!") await message.channel.send("I couldn't extract frames from that GIF, sorry!")
return 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 # Analyze the GIF with tenor_gif media type
video_description = await analyze_video_with_vision(frames, media_type="tenor_gif") video_description = await analyze_video_with_vision(frames, media_type="tenor_gif")
@@ -366,9 +374,9 @@ async def on_message(message):
) )
if is_dm: 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: 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) 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" current_persona = "evil" if globals.EVIL_MODE else "miku"
asyncio.create_task(check_for_interjection(response_message, current_persona)) asyncio.create_task(check_for_interjection(response_message, current_persona))
except Exception as e: except Exception as e:
print(f"⚠️ Error checking for persona interjection: {e}") logger.error(f"Error checking for persona interjection: {e}")
return return
# Handle other types of embeds (rich, article, image, video, link) # Handle other types of embeds (rich, article, image, video, link)
elif embed.type in ['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 # Extract content from embed
embed_content = await extract_embed_content(embed) embed_content = await extract_embed_content(embed)
if not embed_content['has_content']: if not embed_content['has_content']:
print(f"⚠️ Embed has no extractable content, skipping") logger.warning(f"Embed has no extractable content, skipping")
continue continue
# Build context string with embed text # Build context string with embed text
@@ -406,28 +414,28 @@ async def on_message(message):
# Process images from embed # Process images from embed
if embed_content['images']: if embed_content['images']:
for img_url in 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: try:
base64_img = await download_and_encode_image(img_url) base64_img = await download_and_encode_image(img_url)
if base64_img: if base64_img:
print(f"Image downloaded, analyzing with vision model...") logger.info(f"Image downloaded, analyzing with vision model...")
# Analyze image # Analyze image
qwen_description = await analyze_image_with_qwen(base64_img) qwen_description = await analyze_image_with_qwen(base64_img)
truncated = (qwen_description[:50] + "...") if len(qwen_description) > 50 else qwen_description 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(): if qwen_description and qwen_description.strip():
embed_context_parts.append(f"[Embedded image shows: {qwen_description}]") embed_context_parts.append(f"[Embedded image shows: {qwen_description}]")
else: else:
print(f"Failed to download image from embed") logger.error(f"Failed to download image from embed")
except Exception as e: except Exception as e:
print(f"⚠️ Error processing embedded image: {e}") logger.error(f"Error processing embedded image: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
# Process videos from embed # Process videos from embed
if embed_content['videos']: if embed_content['videos']:
for video_url in 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: try:
media_bytes_b64 = await download_and_encode_media(video_url) media_bytes_b64 = await download_and_encode_media(video_url)
if media_bytes_b64: if media_bytes_b64:
@@ -435,17 +443,17 @@ async def on_message(message):
media_bytes = base64.b64decode(media_bytes_b64) media_bytes = base64.b64decode(media_bytes_b64)
frames = await extract_video_frames(media_bytes, num_frames=6) frames = await extract_video_frames(media_bytes, num_frames=6)
if frames: 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") 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(): if video_description and video_description.strip():
embed_context_parts.append(f"[Embedded video shows: {video_description}]") embed_context_parts.append(f"[Embedded video shows: {video_description}]")
else: else:
print(f"Failed to extract frames from video") logger.error(f"Failed to extract frames from video")
else: else:
print(f"Failed to download video from embed") logger.error(f"Failed to download video from embed")
except Exception as e: except Exception as e:
print(f"⚠️ Error processing embedded video: {e}") logger.error(f"Error processing embedded video: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
@@ -468,9 +476,9 @@ async def on_message(message):
) )
if is_dm: 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: 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) 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" current_persona = "evil" if globals.EVIL_MODE else "miku"
asyncio.create_task(check_for_interjection(response_message, current_persona)) asyncio.create_task(check_for_interjection(response_message, current_persona))
except Exception as e: except Exception as e:
print(f"⚠️ Error checking for persona interjection: {e}") logger.error(f"Error checking for persona interjection: {e}")
return return
@@ -494,7 +502,7 @@ async def on_message(message):
is_image_request, image_prompt = await detect_image_request(prompt) is_image_request, image_prompt = await detect_image_request(prompt)
if is_image_request and image_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 # Handle the image generation workflow
success = await handle_image_generation_request(message, image_prompt) success = await handle_image_generation_request(message, image_prompt)
@@ -502,7 +510,7 @@ async def on_message(message):
return # Image generation completed successfully return # Image generation completed successfully
# If image generation failed, fall back to normal response # 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 # If message is just a prompt, no image
# For DMs, pass None as guild_id to use DM mood # For DMs, pass None as guild_id to use DM mood
@@ -518,9 +526,9 @@ async def on_message(message):
) )
if is_dm: 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: 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) 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) # For server messages, check if opposite persona should interject (persona dialogue system)
if not is_dm and globals.BIPOLAR_MODE: 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: try:
from utils.persona_dialogue import check_for_interjection from utils.persona_dialogue import check_for_interjection
current_persona = "evil" if globals.EVIL_MODE else "miku" 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 # Pass the bot's response message for analysis
asyncio.create_task(check_for_interjection(response_message, current_persona)) asyncio.create_task(check_for_interjection(response_message, current_persona))
except Exception as e: except Exception as e:
print(f"⚠️ Error checking for persona interjection: {e}") logger.error(f"Error checking for persona interjection: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
@@ -557,11 +565,11 @@ async def on_message(message):
detected = detect_mood_shift(response, server_context) detected = detect_mood_shift(response, server_context)
if detected and detected != server_config.current_mood_name: 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 # Block direct transitions to asleep unless from sleepy
if detected == "asleep" and server_config.current_mood_name != "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: else:
# Update server mood # Update server mood
server_manager.set_server_mood(message.guild.id, detected) 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 from utils.moods import update_server_nickname
globals.client.loop.create_task(update_server_nickname(message.guild.id)) 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": if detected == "asleep":
server_manager.set_server_sleep_state(message.guild.id, True) 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_sleep_state(message.guild.id, False)
server_manager.set_server_mood(message.guild.id, "neutral") server_manager.set_server_mood(message.guild.id, "neutral")
await update_server_nickname(message.guild.id) 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()) globals.client.loop.create_task(delayed_wakeup())
else: 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: except Exception as e:
print(f"⚠️ Error in server mood detection: {e}") logger.error(f"Error in server mood detection: {e}")
elif is_dm: 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) # V2: Track message for autonomous engine (non-blocking, no LLM calls)
# IMPORTANT: Only call this if the message was NOT addressed to Miku # 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}" 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 @globals.client.event
async def on_raw_reaction_remove(payload): 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}" 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 @globals.client.event
async def on_presence_update(before, after): async def on_presence_update(before, after):
@@ -698,16 +706,18 @@ async def on_member_join(member):
autonomous_member_join(member) autonomous_member_join(member)
def start_api(): 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(): def save_autonomous_state():
"""Save autonomous context on shutdown""" """Save autonomous context on shutdown"""
try: try:
from utils.autonomous import autonomous_engine from utils.autonomous import autonomous_engine
autonomous_engine.save_context() autonomous_engine.save_context()
print("💾 Saved autonomous context on shutdown") logger.info("💾 Saved autonomous context on shutdown")
except Exception as e: 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 # Register shutdown handlers
atexit.register(save_autonomous_state) atexit.register(save_autonomous_state)

View File

@@ -4,17 +4,20 @@ import asyncio
import globals import globals
from utils.moods import load_mood_description from utils.moods import load_mood_description
from utils.scheduled import send_bedtime_reminder from utils.scheduled import send_bedtime_reminder
from utils.logger import get_logger
logger = get_logger('commands')
def set_mood(new_mood: str) -> bool: def set_mood(new_mood: str) -> bool:
"""Set mood (legacy function - now handled per-server or DM)""" """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 return False
def reset_mood() -> str: def reset_mood() -> str:
"""Reset mood to neutral (legacy function - now handled per-server or DM)""" """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" return "neutral"
@@ -24,7 +27,7 @@ def check_mood():
def calm_miku() -> str: def calm_miku() -> str:
"""Calm Miku down (legacy function - now handled per-server or DM)""" """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" return "neutral"
@@ -34,14 +37,14 @@ def reset_conversation(user_id):
async def force_sleep() -> str: async def force_sleep() -> str:
"""Force Miku to sleep (legacy function - now handled per-server or DM)""" """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" return "asleep"
async def wake_up(set_sleep_state=None): async def wake_up(set_sleep_state=None):
reset_mood() reset_mood()
# Note: DMs don't have sleep states, so this is deprecated # 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: if set_sleep_state:
await set_sleep_state(False) 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) success = await update_profile_picture(globals.client, mood=mood)
return success return success
except Exception as e: except Exception as e:
print(f"⚠️ Error updating profile picture: {e}") logger.error(f"Error updating profile picture: {e}")
return False return False

View File

@@ -13,6 +13,9 @@ from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger from apscheduler.triggers.date import DateTrigger
import random import random
from datetime import datetime, timedelta from datetime import datetime, timedelta
from utils.logger import get_logger
logger = get_logger('server')
@dataclass @dataclass
class ServerConfig: class ServerConfig:
@@ -58,7 +61,7 @@ class ServerConfig:
features_list = [f.strip().strip("'\"") for f in features_str.split(',') if f.strip()] features_list = [f.strip().strip("'\"") for f in features_str.split(',') if f.strip()]
data['enabled_features'] = set(features_list) data['enabled_features'] = set(features_list)
except Exception as e: 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 # Fallback to default features
data['enabled_features'] = {"autonomous", "bedtime", "monday_video"} data['enabled_features'] = {"autonomous", "bedtime", "monday_video"}
return cls(**data) return cls(**data)
@@ -83,12 +86,12 @@ class ServerManager:
guild_id = int(guild_id_str) guild_id = int(guild_id_str)
self.servers[guild_id] = ServerConfig.from_dict(server_data) self.servers[guild_id] = ServerConfig.from_dict(server_data)
self.server_memories[guild_id] = {} 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 # After loading, check if we need to repair the config
self.repair_config() self.repair_config()
except Exception as e: 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() self._create_default_config()
else: else:
self._create_default_config() self._create_default_config()
@@ -101,21 +104,21 @@ class ServerManager:
# Check if enabled_features is a string (corrupted) # Check if enabled_features is a string (corrupted)
if isinstance(server.enabled_features, str): if isinstance(server.enabled_features, str):
needs_repair = True 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 # Re-parse the features
try: try:
features_str = server.enabled_features.strip('{}') features_str = server.enabled_features.strip('{}')
features_list = [f.strip().strip("'\"") for f in features_str.split(',') if f.strip()] features_list = [f.strip().strip("'\"") for f in features_str.split(',') if f.strip()]
server.enabled_features = set(features_list) server.enabled_features = set(features_list)
except Exception as e: 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"} server.enabled_features = {"autonomous", "bedtime", "monday_video"}
if needs_repair: if needs_repair:
print("🔧 Saving repaired configuration...") logger.info("Saving repaired configuration...")
self.save_config() self.save_config()
except Exception as e: except Exception as e:
print(f"⚠️ Failed to repair config: {e}") logger.error(f"Failed to repair config: {e}")
def _create_default_config(self): def _create_default_config(self):
"""Create default configuration for backward compatibility""" """Create default configuration for backward compatibility"""
@@ -132,7 +135,7 @@ class ServerManager:
self.servers[default_server.guild_id] = default_server self.servers[default_server.guild_id] = default_server
self.server_memories[default_server.guild_id] = {} self.server_memories[default_server.guild_id] = {}
self.save_config() self.save_config()
print("📋 Created default server configuration") logger.info("Created default server configuration")
def save_config(self): def save_config(self):
"""Save server configurations to file""" """Save server configurations to file"""
@@ -150,14 +153,14 @@ class ServerManager:
with open(self.config_file, "w", encoding="utf-8") as f: with open(self.config_file, "w", encoding="utf-8") as f:
json.dump(config_data, f, indent=2) json.dump(config_data, f, indent=2)
except Exception as e: 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, def add_server(self, guild_id: int, guild_name: str, autonomous_channel_id: int,
autonomous_channel_name: str, bedtime_channel_ids: List[int] = None, autonomous_channel_name: str, bedtime_channel_ids: List[int] = None,
enabled_features: Set[str] = None) -> bool: enabled_features: Set[str] = None) -> bool:
"""Add a new server configuration""" """Add a new server configuration"""
if guild_id in self.servers: if guild_id in self.servers:
print(f"⚠️ Server {guild_id} already exists") logger.info(f"Server {guild_id} already exists")
return False return False
if bedtime_channel_ids is None: if bedtime_channel_ids is None:
@@ -178,7 +181,7 @@ class ServerManager:
self.servers[guild_id] = server self.servers[guild_id] = server
self.server_memories[guild_id] = {} self.server_memories[guild_id] = {}
self.save_config() 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 return True
def remove_server(self, guild_id: int) -> bool: def remove_server(self, guild_id: int) -> bool:
@@ -199,7 +202,7 @@ class ServerManager:
del self.server_memories[guild_id] del self.server_memories[guild_id]
self.save_config() self.save_config()
print(f"🗑️ Removed server: {server_name} (ID: {guild_id})") logger.info(f"Removed server: {server_name} (ID: {guild_id})")
return True return True
def get_server_config(self, guild_id: int) -> Optional[ServerConfig]: def get_server_config(self, guild_id: int) -> Optional[ServerConfig]:
@@ -221,7 +224,7 @@ class ServerManager:
setattr(server, key, value) setattr(server, key, value)
self.save_config() self.save_config()
print(f"Updated config for server: {server.guild_name}") logger.info(f"Updated config for server: {server.guild_name}")
return True return True
def get_server_memory(self, guild_id: int, key: str = None): def get_server_memory(self, guild_id: int, key: str = None):
@@ -267,12 +270,12 @@ class ServerManager:
from utils.moods import load_mood_description from utils.moods import load_mood_description
server.current_mood_description = load_mood_description(mood_name) server.current_mood_description = load_mood_description(mood_name)
except Exception as e: 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." server.current_mood_description = f"I'm feeling {mood_name} today."
self.save_config() self.save_config()
print(f"😊 Server {server.guild_name} mood changed to: {mood_name}") logger.info(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.debug(f"Mood description: {server.current_mood_description[:100]}{'...' if len(server.current_mood_description) > 100 else ''}")
return True return True
def get_server_sleep_state(self, guild_id: int) -> bool: 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): def setup_server_scheduler(self, guild_id: int, client: discord.Client):
"""Setup independent scheduler for a specific server""" """Setup independent scheduler for a specific server"""
if guild_id not in self.servers: 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 return
server_config = self.servers[guild_id] server_config = self.servers[guild_id]
@@ -363,8 +366,8 @@ class ServerManager:
# Add bedtime reminder job # Add bedtime reminder job
if "bedtime" in server_config.enabled_features: if "bedtime" in server_config.enabled_features:
print(f"Setting up bedtime scheduler for server {server_config.guild_name}") logger.info(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.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( scheduler.add_job(
self._schedule_random_bedtime_for_server, self._schedule_random_bedtime_for_server,
CronTrigger(hour=server_config.bedtime_hour, minute=server_config.bedtime_minute), CronTrigger(hour=server_config.bedtime_hour, minute=server_config.bedtime_minute),
@@ -382,11 +385,11 @@ class ServerManager:
self.schedulers[guild_id] = scheduler self.schedulers[guild_id] = scheduler
scheduler.start() 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): def start_all_schedulers(self, client: discord.Client):
"""Start schedulers for all servers""" """Start schedulers for all servers"""
print("🚀 Starting all server schedulers...") logger.info("Starting all server schedulers...")
for guild_id in self.servers: for guild_id in self.servers:
self.setup_server_scheduler(guild_id, client) self.setup_server_scheduler(guild_id, client)
@@ -396,42 +399,42 @@ class ServerManager:
# Start Figurine DM scheduler # Start Figurine DM scheduler
self.setup_figurine_updates_scheduler(client) 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): 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""" """Update just the bedtime job for a specific server without restarting all schedulers"""
server_config = self.servers.get(guild_id) server_config = self.servers.get(guild_id)
if not server_config: 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 return False
scheduler = self.schedulers.get(guild_id) scheduler = self.schedulers.get(guild_id)
if not scheduler: if not scheduler:
print(f"⚠️ No scheduler found for guild {guild_id}") logger.warning(f"No scheduler found for guild {guild_id}")
return False return False
# Remove existing bedtime job if it exists # Remove existing bedtime job if it exists
bedtime_job_id = f"bedtime_schedule_{guild_id}" bedtime_job_id = f"bedtime_schedule_{guild_id}"
try: try:
scheduler.remove_job(bedtime_job_id) 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: 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 # Add new bedtime job with updated configuration
if "bedtime" in server_config.enabled_features: if "bedtime" in server_config.enabled_features:
print(f"Updating bedtime scheduler for server {server_config.guild_name}") logger.info(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.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( scheduler.add_job(
self._schedule_random_bedtime_for_server, self._schedule_random_bedtime_for_server,
CronTrigger(hour=server_config.bedtime_hour, minute=server_config.bedtime_minute), CronTrigger(hour=server_config.bedtime_hour, minute=server_config.bedtime_minute),
args=[guild_id, client], args=[guild_id, client],
id=bedtime_job_id 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 return True
else: else:
print(f" Bedtime feature not enabled for server {guild_id}") logger.info(f"Bedtime feature not enabled for server {guild_id}")
return True return True
def setup_dm_mood_scheduler(self, client: discord.Client): def setup_dm_mood_scheduler(self, client: discord.Client):
@@ -449,10 +452,10 @@ class ServerManager:
scheduler.start() scheduler.start()
self.schedulers["dm_mood"] = scheduler 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: 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): def _enqueue_figurine_send(self, client: discord.Client):
"""Enqueue the figurine DM send task in the client's loop.""" """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 from utils.figurine_notifier import send_figurine_dm_to_all_subscribers
if client.loop and client.loop.is_running(): if client.loop and client.loop.is_running():
client.loop.create_task(send_figurine_dm_to_all_subscribers(client)) 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: else:
print("⚠️ Client loop not available for figurine DM send") logger.warning("Client loop not available for figurine DM send")
except Exception as e: 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): 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).""" """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) target_time = now.replace(hour=random_hour, minute=random_minute, second=0, microsecond=0)
if target_time <= now: if target_time <= now:
target_time = target_time + timedelta(days=1) 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( scheduler.add_job(
self._enqueue_figurine_send, self._enqueue_figurine_send,
DateTrigger(run_date=target_time), DateTrigger(run_date=target_time),
@@ -499,22 +502,22 @@ class ServerManager:
self._schedule_one_figurine_send_today(scheduler, client) self._schedule_one_figurine_send_today(scheduler, client)
scheduler.start() scheduler.start()
self.schedulers["figurine_dm"] = scheduler self.schedulers["figurine_dm"] = scheduler
print("🗓️ Figurine updates scheduler started") logger.info("Figurine updates scheduler started")
except Exception as e: 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): def stop_all_schedulers(self):
"""Stop all schedulers""" """Stop all schedulers"""
print("🛑 Stopping all schedulers...") logger.info("Stopping all schedulers...")
for scheduler in self.schedulers.values(): for scheduler in self.schedulers.values():
try: try:
scheduler.shutdown() scheduler.shutdown()
except Exception as e: except Exception as e:
print(f"⚠️ Error stopping scheduler: {e}") logger.warning(f"Error stopping scheduler: {e}")
self.schedulers.clear() self.schedulers.clear()
print("All schedulers stopped") logger.info("All schedulers stopped")
# Implementation of autonomous functions - these integrate with the autonomous system # Implementation of autonomous functions - these integrate with the autonomous system
def _run_autonomous_for_server(self, guild_id: int, client: discord.Client): 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 # Create an async task in the client's event loop
if client.loop and client.loop.is_running(): if client.loop and client.loop.is_running():
client.loop.create_task(autonomous_tick(guild_id)) 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: 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: 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): def _run_autonomous_reaction_for_server(self, guild_id: int, client: discord.Client):
"""Run autonomous reaction for a specific server - called by APScheduler""" """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 # Create an async task in the client's event loop
if client.loop and client.loop.is_running(): if client.loop and client.loop.is_running():
client.loop.create_task(autonomous_reaction_tick(guild_id)) 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: 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: 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): def _run_conversation_detection_for_server(self, guild_id: int, client: discord.Client):
"""Run conversation detection for a specific server - called by APScheduler""" """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 # Create an async task in the client's event loop
if client.loop and client.loop.is_running(): if client.loop and client.loop.is_running():
client.loop.create_task(miku_detect_and_join_conversation_for_server(guild_id)) 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: 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: 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): def _send_monday_video_for_server(self, guild_id: int, client: discord.Client):
"""Send Monday video for a specific server - called by APScheduler""" """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 # Create an async task in the client's event loop
if client.loop and client.loop.is_running(): if client.loop and client.loop.is_running():
client.loop.create_task(send_monday_video_for_server(guild_id)) 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: 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: 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): 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""" """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 # Get server config to determine the random time range
server_config = self.servers.get(guild_id) server_config = self.servers.get(guild_id)
if not server_config: 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 return
# Calculate random time within the bedtime range # Calculate random time within the bedtime range
start_minutes = server_config.bedtime_hour * 60 + server_config.bedtime_minute start_minutes = server_config.bedtime_hour * 60 + server_config.bedtime_minute
end_minutes = server_config.bedtime_hour_end * 60 + server_config.bedtime_minute_end 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) # Handle case where end time is next day (e.g., 23:30 to 00:30)
if end_minutes <= start_minutes: if end_minutes <= start_minutes:
end_minutes += 24 * 60 # Add 24 hours 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) 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 # Convert back to hours and minutes
random_hour = (random_minutes // 60) % 24 random_hour = (random_minutes // 60) % 24
@@ -609,7 +612,7 @@ class ServerManager:
delay_seconds = (target_time - now).total_seconds() 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 # Schedule the actual bedtime reminder
try: try:
@@ -618,9 +621,9 @@ class ServerManager:
def send_bedtime_delayed(): def send_bedtime_delayed():
if client.loop and client.loop.is_running(): if client.loop and client.loop.is_running():
client.loop.create_task(send_bedtime_reminder_for_server(guild_id, client)) 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: 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 # Use the scheduler to schedule the delayed bedtime reminder
scheduler = self.schedulers.get(guild_id) scheduler = self.schedulers.get(guild_id)
@@ -630,12 +633,12 @@ class ServerManager:
DateTrigger(run_date=target_time), DateTrigger(run_date=target_time),
id=f"bedtime_reminder_{guild_id}_{int(target_time.timestamp())}" 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: else:
print(f"⚠️ No scheduler found for server {guild_id}") logger.warning(f"No scheduler found for server {guild_id}")
except Exception as e: 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): def _rotate_server_mood(self, guild_id: int, client: discord.Client):
"""Rotate mood for a specific server - called by APScheduler""" """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 # Create an async task in the client's event loop
if client.loop and client.loop.is_running(): if client.loop and client.loop.is_running():
client.loop.create_task(rotate_server_mood(guild_id)) 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: 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: 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 # Global instance
server_manager = ServerManager() server_manager = ServerManager()

View File

@@ -658,12 +658,13 @@
<div class="tab-container"> <div class="tab-container">
<div class="tab-buttons"> <div class="tab-buttons">
<button class="tab-button active" onclick="switchTab('tab1')">Server Management</button> <button class="tab-button active" onclick="switchTab('tab1')">Server Management</button>
<button class="tab-button" onclick="switchTab('tab2')">Actions</button> <button class="tab-button" onclick="switchTab('tab2')">Actions</button>
<button class="tab-button" onclick="switchTab('tab3')">Status</button> <button class="tab-button" onclick="switchTab('tab3')">Status</button>
<button class="tab-button" onclick="switchTab('tab4')">🎨 Image Generation</button> <button class="tab-button" onclick="switchTab('tab4')">🎨 Image Generation</button>
<button class="tab-button" onclick="switchTab('tab5')">📊 Autonomous Stats</button> <button class="tab-button" onclick="switchTab('tab5')">📊 Autonomous Stats</button>
<button class="tab-button" onclick="switchTab('tab6')">💬 Chat with LLM</button> <button class="tab-button" onclick="switchTab('tab6')">💬 Chat with LLM</button>
</div> <button class="tab-button" onclick="window.location.href='/static/system.html'">🎛️ System Settings</button>
</div>
<!-- Tab 1 Content --> <!-- Tab 1 Content -->
<div id="tab1" class="tab-content active"> <div id="tab1" class="tab-content active">

415
bot/static/system-logic.js Normal file
View File

@@ -0,0 +1,415 @@
let currentConfig = null;
let componentsData = null;
// Load configuration on page load
window.addEventListener('DOMContentLoaded', () => {
loadConfiguration();
loadComponents();
});
async function loadConfiguration() {
try {
const response = await fetch('/api/log/config');
const data = await response.json();
if (data.success) {
currentConfig = data.config;
// Load timestamp format setting
const timestampFormat = data.config.formatting?.timestamp_format || 'datetime';
const timestampSelect = document.getElementById('timestampFormat');
if (timestampSelect) {
timestampSelect.value = timestampFormat;
}
} else {
showNotification('Failed to load configuration', 'error');
}
} catch (error) {
showNotification('Error loading configuration: ' + error.message, 'error');
}
}
async function loadComponents() {
try {
const response = await fetch('/api/log/components');
const data = await response.json();
if (data.success) {
componentsData = data;
renderComponentsTable();
populatePreviewSelect();
} else {
showNotification('Failed to load components', 'error');
}
} catch (error) {
showNotification('Error loading components: ' + error.message, 'error');
}
}
function renderComponentsTable() {
const tbody = document.getElementById('componentsTable');
tbody.innerHTML = '';
for (const [name, description] of Object.entries(componentsData.components)) {
const stats = componentsData.stats[name] || {};
const enabled = stats.enabled !== undefined ? stats.enabled : true;
const enabledLevels = stats.enabled_levels || ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
const allLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
if (name === 'api.requests') {
allLevels.push('API');
}
const levelCheckboxes = allLevels.map(level => {
const emoji = {'DEBUG': '🔍', 'INFO': '', 'WARNING': '⚠️', 'ERROR': '❌', 'CRITICAL': '🔥', 'API': '🌐'}[level];
const checked = enabledLevels.includes(level) ? 'checked' : '';
return `
<div class="level-checkbox">
<input type="checkbox"
id="level_${name}_${level}"
${checked}
onchange="updateComponentLevels('${name}')">
<label for="level_${name}_${level}">${emoji} ${level}</label>
</div>
`;
}).join('');
const row = document.createElement('tr');
row.innerHTML = `
<td>
<div style="color: #61dafb; font-weight: bold;">${name}</div>
<div class="component-description">${description}</div>
</td>
<td>
<label class="toggle">
<input type="checkbox" id="enabled_${name}" ${enabled ? 'checked' : ''} onchange="updateComponentEnabled('${name}')">
<span class="slider"></span>
</label>
</td>
<td>
<div class="level-checkboxes">
${levelCheckboxes}
</div>
</td>
<td>
<span class="status-indicator ${enabled ? 'status-active' : 'status-inactive'}"></span>
${enabled ? 'Active' : 'Inactive'}
</td>
`;
tbody.appendChild(row);
if (name === 'api.requests') {
document.getElementById('enabled_' + name).addEventListener('change', (e) => {
document.getElementById('apiFilters').style.display = e.target.checked ? 'block' : 'none';
});
if (enabled) {
document.getElementById('apiFilters').style.display = 'block';
loadApiFilters();
}
}
}
// Update global level checkboxes based on current state
updateGlobalLevelCheckboxes();
}
function updateGlobalLevelCheckboxes() {
const allLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'API'];
for (const level of allLevels) {
let allComponentsHaveLevel = true;
// Check if ALL components have this level enabled
for (const [name, description] of Object.entries(componentsData.components)) {
const stats = componentsData.stats[name] || {};
const enabledLevels = stats.enabled_levels || [];
// Skip API level for non-api.requests components
if (level === 'API' && name !== 'api.requests') {
continue;
}
if (!enabledLevels.includes(level)) {
allComponentsHaveLevel = false;
break;
}
}
const checkbox = document.getElementById('global_' + level);
if (checkbox) {
checkbox.checked = allComponentsHaveLevel;
}
}
}
function populatePreviewSelect() {
const select = document.getElementById('previewComponent');
select.innerHTML = '';
for (const name of Object.keys(componentsData.components)) {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
select.appendChild(option);
}
loadLogPreview();
}
async function updateComponentEnabled(component) {
const enabled = document.getElementById('enabled_' + component).checked;
try {
const response = await fetch('/api/log/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
component: component,
enabled: enabled
})
});
const data = await response.json();
if (data.success) {
showNotification(`${enabled ? 'Enabled' : 'Disabled'} ${component}`, 'success');
const row = document.getElementById('enabled_' + component).closest('tr');
const statusCell = row.querySelector('td:last-child');
statusCell.innerHTML = `
<span class="status-indicator ${enabled ? 'status-active' : 'status-inactive'}"></span>
${enabled ? 'Active' : 'Inactive'}
`;
} else {
showNotification('Failed to update ' + component + ': ' + data.error, 'error');
}
} catch (error) {
showNotification('Error updating component: ' + error.message, 'error');
}
}
async function updateGlobalLevel(level, enabled) {
try {
const response = await fetch(`/api/log/global-level?level=${level}&enabled=${enabled}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
const data = await response.json();
if (data.success) {
const action = enabled ? 'enabled' : 'disabled';
showNotification(`${level} ${action} globally across all components`, 'success');
// Reload components to reflect changes
await loadComponents();
} else {
showNotification('Failed to update global level: ' + data.error, 'error');
}
} catch (error) {
showNotification('Error updating global level: ' + error.message, 'error');
}
}
async function updateTimestampFormat(format) {
try {
const response = await fetch(`/api/log/timestamp-format?format_type=${format}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
const data = await response.json();
if (data.success) {
showNotification(`Timestamp format updated: ${format}`, 'success');
// Reload all loggers to apply the change
await fetch('/api/log/reload', { method: 'POST' });
} else {
showNotification('Failed to update timestamp format: ' + data.error, 'error');
}
} catch (error) {
showNotification('Error updating timestamp format: ' + error.message, 'error');
}
}
async function updateComponentLevels(component) {
const allLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
if (component === 'api.requests') {
allLevels.push('API');
}
const enabledLevels = allLevels.filter(level => {
const checkbox = document.getElementById(`level_${component}_${level}`);
return checkbox && checkbox.checked;
});
try {
const response = await fetch('/api/log/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
component: component,
enabled_levels: enabledLevels
})
});
const data = await response.json();
if (data.success) {
showNotification(`Updated levels for ${component}: ${enabledLevels.join(', ')}`, 'success');
// Update global level checkboxes to reflect current state
updateGlobalLevelCheckboxes();
} else {
showNotification('Failed to update ' + component + ': ' + data.error, 'error');
}
} catch (error) {
showNotification('Error updating component: ' + error.message, 'error');
}
}
async function loadApiFilters() {
if (!currentConfig || !currentConfig.components['api.requests']) return;
const filters = currentConfig.components['api.requests'].filters || {};
document.getElementById('excludePaths').value = (filters.exclude_paths || []).join(', ');
document.getElementById('excludeStatus').value = (filters.exclude_status || []).join(', ');
document.getElementById('includeSlowRequests').checked = filters.include_slow_requests !== false;
document.getElementById('slowThreshold').value = filters.slow_threshold_ms || 1000;
}
async function saveApiFilters() {
const excludePaths = document.getElementById('excludePaths').value
.split(',')
.map(s => s.trim())
.filter(s => s.length > 0);
const excludeStatus = document.getElementById('excludeStatus').value
.split(',')
.map(s => parseInt(s.trim()))
.filter(n => !isNaN(n));
const includeSlowRequests = document.getElementById('includeSlowRequests').checked;
const slowThreshold = parseInt(document.getElementById('slowThreshold').value);
try {
const response = await fetch('/api/log/filters', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
exclude_paths: excludePaths,
exclude_status: excludeStatus,
include_slow_requests: includeSlowRequests,
slow_threshold_ms: slowThreshold
})
});
const data = await response.json();
if (data.success) {
showNotification('API filters saved', 'success');
} else {
showNotification('Failed to save filters: ' + data.error, 'error');
}
} catch (error) {
showNotification('Error saving filters: ' + error.message, 'error');
}
}
async function saveAllSettings() {
try {
const response = await fetch('/api/log/reload', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification('All settings saved and reloaded', 'success');
await loadConfiguration();
await loadComponents();
} else {
showNotification('Failed to reload settings: ' + data.error, 'error');
}
} catch (error) {
showNotification('Error saving settings: ' + error.message, 'error');
}
}
async function resetToDefaults() {
if (!confirm('Are you sure you want to reset all logging settings to defaults?')) {
return;
}
try {
const response = await fetch('/api/log/reset', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification('Settings reset to defaults', 'success');
await loadConfiguration();
await loadComponents();
} else {
showNotification('Failed to reset settings: ' + data.error, 'error');
}
} catch (error) {
showNotification('Error resetting settings: ' + error.message, 'error');
}
}
async function loadLogPreview() {
const component = document.getElementById('previewComponent').value;
const preview = document.getElementById('logPreview');
preview.innerHTML = '<div class="loading">Loading logs...</div>';
try {
const response = await fetch(`/api/log/files/${component}?lines=50`);
const data = await response.json();
if (data.success) {
if (data.lines.length === 0) {
preview.innerHTML = '<div class="loading">No logs yet for this component</div>';
} else {
preview.innerHTML = data.lines.map(line =>
`<div class="log-line">${escapeHtml(line)}</div>`
).join('');
preview.scrollTop = preview.scrollHeight;
}
} else {
preview.innerHTML = `<div class="loading">Error: ${data.error}</div>`;
}
} catch (error) {
preview.innerHTML = `<div class="loading">Error loading logs: ${error.message}</div>`;
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showNotification(message, type) {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
// Auto-refresh log preview every 5 seconds
setInterval(() => {
if (document.getElementById('previewComponent').value) {
loadLogPreview();
}
}, 5000);

408
bot/static/system.html Normal file
View File

@@ -0,0 +1,408 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🎛️ System Settings - Logging Configuration</title>
<style>
body {
margin: 0;
font-family: monospace;
background-color: #121212;
color: #fff;
}
.container {
padding: 2rem;
max-width: 1600px;
margin: 0 auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #333;
}
h1 {
color: #61dafb;
margin: 0;
font-size: 1.8rem;
}
h2 {
color: #61dafb;
font-size: 1.3rem;
margin: 0 0 1rem 0;
}
.header-actions {
display: flex;
gap: 0.5rem;
}
button, select {
padding: 0.4rem 0.8rem;
background: #333;
color: #fff;
border: 1px solid #555;
font-family: monospace;
cursor: pointer;
font-size: 0.9rem;
}
button:hover, select:hover {
background: #444;
border-color: #666;
}
.btn-primary {
background: #61dafb;
color: #000;
border-color: #61dafb;
font-weight: bold;
}
.btn-primary:hover {
background: #4fa8c5;
}
.btn-secondary {
background: #555;
border-color: #666;
}
.btn-danger {
background: #d32f2f;
border-color: #d32f2f;
}
.btn-danger:hover {
background: #b71c1c;
}
.content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.card {
background: #1e1e1e;
border: 1px solid #333;
border-radius: 8px;
padding: 1.5rem;
}
.components-table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
.components-table th {
background: #2a2a2a;
color: #61dafb;
padding: 0.8rem;
text-align: left;
font-weight: bold;
border-bottom: 2px solid #444;
}
.components-table td {
padding: 0.8rem;
border-bottom: 1px solid #2a2a2a;
vertical-align: top;
}
.components-table tr:hover {
background: #252525;
}
.component-description {
font-size: 0.8rem;
color: #999;
margin-top: 0.3rem;
}
.toggle {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #555;
transition: 0.3s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #61dafb;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.level-checkboxes {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.level-checkbox {
display: flex;
align-items: center;
gap: 0.3rem;
}
.level-checkbox input[type="checkbox"] {
width: auto;
margin: 0;
cursor: pointer;
}
.level-checkbox label {
font-size: 0.85rem;
cursor: pointer;
}
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 0.3rem;
}
.status-active {
background-color: #4CAF50;
}
.status-inactive {
background-color: #555;
}
.api-filters {
background: #2a2a2a;
border: 1px solid #444;
padding: 1rem;
margin-top: 1rem;
border-radius: 8px;
}
.api-filters h3 {
color: #61dafb;
font-size: 1rem;
margin-bottom: 0.8rem;
}
.filter-row {
margin-bottom: 0.8rem;
}
.filter-row label {
display: block;
font-weight: bold;
margin-bottom: 0.3rem;
color: #ccc;
}
.setting-row {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.8rem;
}
.setting-row label {
font-weight: bold;
color: #ccc;
}
input[type="text"], input[type="number"] {
width: 100%;
padding: 0.5rem;
background: #333;
color: #fff;
border: 1px solid #555;
font-family: monospace;
}
.log-preview {
background: #000;
color: #0f0;
padding: 1rem;
border-radius: 8px;
font-family: monospace;
font-size: 0.85rem;
max-height: 600px;
overflow-y: auto;
white-space: pre-wrap;
word-wrap: break-word;
border: 1px solid #333;
}
.log-preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.log-line {
margin-bottom: 3px;
line-height: 1.4;
}
.notification {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #222;
color: #fff;
padding: 1rem;
border: 1px solid #555;
border-radius: 8px;
opacity: 0.95;
z-index: 1000;
font-size: 0.9rem;
animation: slideIn 0.3s ease-out;
}
.notification-success {
border-color: #4CAF50;
background: #1b4d1b;
}
.notification-error {
border-color: #d32f2f;
background: #4d1b1b;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.loading {
text-align: center;
padding: 2rem;
color: #999;
}
@media (max-width: 1200px) {
.content {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎛️ System Settings - Logging Configuration</h1>
<div class="header-actions">
<button class="btn-secondary" onclick="window.location.href='/'">← Back to Dashboard</button>
<button class="btn-primary" onclick="saveAllSettings()">💾 Save All</button>
<button class="btn-danger" onclick="resetToDefaults()">🔄 Reset to Defaults</button>
</div>
</div>
<div class="content">
<div class="card">
<h2>📊 Logging Components</h2>
<p style="color: #999; margin-bottom: 1rem; font-size: 0.9rem;">
Enable or disable specific log levels for each component. You can toggle any combination of levels.
</p>
<div class="api-filters" style="margin-bottom: 1.5rem;">
<h3>🌍 Global Level Controls</h3>
<p style="color: #999; font-size: 0.85rem; margin-bottom: 0.8rem;">
Quickly enable/disable a log level across all components
</p>
<div class="level-checkboxes">
<div class="level-checkbox">
<input type="checkbox" id="global_DEBUG" checked onchange="updateGlobalLevel('DEBUG', this.checked)">
<label for="global_DEBUG">🔍 DEBUG</label>
</div>
<div class="level-checkbox">
<input type="checkbox" id="global_INFO" checked onchange="updateGlobalLevel('INFO', this.checked)">
<label for="global_INFO"> INFO</label>
</div>
<div class="level-checkbox">
<input type="checkbox" id="global_WARNING" checked onchange="updateGlobalLevel('WARNING', this.checked)">
<label for="global_WARNING">⚠️ WARNING</label>
</div>
<div class="level-checkbox">
<input type="checkbox" id="global_ERROR" checked onchange="updateGlobalLevel('ERROR', this.checked)">
<label for="global_ERROR">❌ ERROR</label>
</div>
<div class="level-checkbox">
<input type="checkbox" id="global_CRITICAL" checked onchange="updateGlobalLevel('CRITICAL', this.checked)">
<label for="global_CRITICAL">🔥 CRITICAL</label>
</div>
<div class="level-checkbox">
<input type="checkbox" id="global_API" checked onchange="updateGlobalLevel('API', this.checked)">
<label for="global_API">🌐 API</label>
</div>
</div>
</div>
<div class="api-filters" style="margin-bottom: 1.5rem;">
<h3>🕐 Timestamp Format</h3>
<p style="color: #999; font-size: 0.85rem; margin-bottom: 0.8rem;">
Control how timestamps appear in logs
</p>
<div class="setting-row">
<label>Format:</label>
<select id="timestampFormat" onchange="updateTimestampFormat(this.value)">
<option value="datetime">Date + Time (2026-01-10 20:30:45)</option>
<option value="time">Time Only (20:30:45)</option>
<option value="date">Date Only (2026-01-10)</option>
<option value="off">No Timestamp</option>
</select>
</div>
</div>
<table class="components-table">
<thead>
<tr>
<th>Component</th>
<th>Enabled</th>
<th>Log Levels</th>
<th>Status</th>
</tr>
</thead>
<tbody id="componentsTable">
<tr><td colspan="4" class="loading">Loading components...</td></tr>
</tbody>
</table>
<div id="apiFilters" class="api-filters" style="display: none;">
<h3>🌐 API Request Filters</h3>
<div class="filter-row">
<label>Exclude Paths (comma-separated):</label>
<input type="text" id="excludePaths" placeholder="/health, /static/*">
</div>
<div class="filter-row">
<label>Exclude Status Codes (comma-separated):</label>
<input type="text" id="excludeStatus" placeholder="200, 304">
</div>
<div class="setting-row">
<label>Log Slow Requests (>1000ms):</label>
<label class="toggle">
<input type="checkbox" id="includeSlowRequests" checked>
<span class="slider"></span>
</label>
</div>
<div class="filter-row">
<label>Slow Request Threshold (ms):</label>
<input type="number" id="slowThreshold" value="1000" min="100" step="100">
</div>
<button class="btn-primary" onclick="saveApiFilters()" style="margin-top: 0.5rem;">Save API Filters</button>
</div>
</div>
<div class="card">
<h2>📜 Live Log Preview</h2>
<div class="log-preview-header">
<div>
<label>Component: </label>
<select id="previewComponent" onchange="loadLogPreview()"><option value="bot">Bot</option></select>
</div>
<button class="btn-secondary" onclick="loadLogPreview()">🔄 Refresh</button>
</div>
<div class="log-preview" id="logPreview">
<div class="loading">Select a component to view logs...</div>
</div>
</div>
</div>
</div>
<script src="/static/system-logic.js"></script>
</body>
</html>

View File

@@ -9,6 +9,9 @@ import time
from utils.autonomous_engine import autonomous_engine from utils.autonomous_engine import autonomous_engine
from server_manager import server_manager from server_manager import server_manager
import globals import globals
from utils.logger import get_logger
logger = get_logger('autonomous')
# Rate limiting: Track last action time per server to prevent rapid-fire # Rate limiting: Track last action time per server to prevent rapid-fire
_last_action_execution = {} # guild_id -> timestamp _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: if guild_id in _last_action_execution:
time_since_last = now - _last_action_execution[guild_id] time_since_last = now - _last_action_execution[guild_id]
if time_since_last < _MIN_ACTION_INTERVAL: 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 return
# Ask the engine if Miku should act (with optional debug logging) # 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 # Engine decided not to act
return 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 # Execute the action using legacy functions
from utils.autonomous_v1_legacy import ( from utils.autonomous_v1_legacy import (
@@ -58,12 +61,12 @@ async def autonomous_tick_v2(guild_id: int):
elif action_type == "change_profile_picture": elif action_type == "change_profile_picture":
# Get current mood for this server # Get current mood for this server
mood, _ = server_manager.get_server_mood(guild_id) 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) result = await profile_picture_manager.change_profile_picture(mood=mood, debug=True)
if result["success"]: if result["success"]:
print(f"Profile picture changed successfully!") logger.info(f"Profile picture changed successfully!")
else: 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 # Record that action was taken
autonomous_engine.record_action(guild_id) autonomous_engine.record_action(guild_id)
@@ -84,10 +87,10 @@ async def autonomous_tick_v2(guild_id: int):
if channel: if channel:
await maybe_trigger_argument(channel, globals.client, "Triggered after an autonomous action") await maybe_trigger_argument(channel, globals.client, "Triggered after an autonomous action")
except Exception as bipolar_err: except Exception as bipolar_err:
print(f"⚠️ Bipolar check error: {bipolar_err}") logger.warning(f"Bipolar check error: {bipolar_err}")
except Exception as e: 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): 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: if not should_react:
return 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: try:
from utils.autonomous_v1_legacy import miku_autonomous_reaction_for_server 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) autonomous_engine.record_action(guild_id)
except Exception as e: except Exception as e:
print(f"⚠️ Error executing scheduled reaction: {e}") logger.error(f"Error executing scheduled reaction: {e}")
def on_message_event(message): 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) should_react = autonomous_engine.should_react_to_message(guild_id, message_age)
if should_react: 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 from utils.autonomous_v1_legacy import miku_autonomous_reaction_for_server
await miku_autonomous_reaction_for_server(guild_id, force_message=message) 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) action_type = autonomous_engine.should_take_action(guild_id, triggered_by_message=True)
if action_type: 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) # Execute the action directly (don't call autonomous_tick_v2 which would check again)
from utils.autonomous_v1_legacy import ( from utils.autonomous_v1_legacy import (
@@ -209,12 +212,12 @@ async def _check_and_act(guild_id: int):
elif action_type == "change_profile_picture": elif action_type == "change_profile_picture":
# Get current mood for this server # Get current mood for this server
mood, _ = server_manager.get_server_mood(guild_id) 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) result = await profile_picture_manager.change_profile_picture(mood=mood, debug=True)
if result["success"]: if result["success"]:
print(f"Profile picture changed successfully!") logger.info(f"Profile picture changed successfully!")
else: 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 # Record that action was taken
autonomous_engine.record_action(guild_id) autonomous_engine.record_action(guild_id)
@@ -232,10 +235,10 @@ async def _check_and_act(guild_id: int):
if channel: if channel:
await maybe_trigger_argument(channel, globals.client, "Triggered after message-based action") await maybe_trigger_argument(channel, globals.client, "Triggered after message-based action")
except Exception as bipolar_err: except Exception as bipolar_err:
print(f"⚠️ Bipolar check error: {bipolar_err}") logger.warning(f"Bipolar check error: {bipolar_err}")
except Exception as e: 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): def on_presence_update(member, before, after):
@@ -256,7 +259,7 @@ def on_presence_update(member, before, after):
# Track status changes # Track status changes
if before.status != after.status: if before.status != after.status:
autonomous_engine.track_user_event(guild_id, "status_changed") 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 # Track activity changes
if before.activities != after.activities: if before.activities != after.activities:
@@ -272,7 +275,7 @@ def on_presence_update(member, before, after):
"activity_started", "activity_started",
{"activity_name": activity_name} {"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): def on_member_join(member):
@@ -310,17 +313,17 @@ async def periodic_decay_task():
try: try:
autonomous_engine.decay_events(guild_id) autonomous_engine.decay_events(guild_id)
except Exception as e: 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 # Save context to disk periodically
try: try:
autonomous_engine.save_context() autonomous_engine.save_context()
except Exception as e: 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 uptime_hours = (time.time() - task_start_time) / 3600
print(f"🧹 [V2] Decay task completed (iteration #{iteration_count}, uptime: {uptime_hours:.1f}h)") logger.debug(f"[V2] Decay task completed (iteration #{iteration_count}, uptime: {uptime_hours:.1f}h)")
print(f" └─ Processed {len(guild_ids)} servers") logger.debug(f" └─ Processed {len(guild_ids)} servers")
def initialize_v2_system(client): def initialize_v2_system(client):
@@ -328,7 +331,7 @@ def initialize_v2_system(client):
Initialize the V2 autonomous system. Initialize the V2 autonomous system.
Call this from bot.py on startup. Call this from bot.py on startup.
""" """
print("🚀 Initializing Autonomous V2 System...") logger.debug("Initializing Autonomous V2 System...")
# Initialize mood states for all servers # Initialize mood states for all servers
for guild_id, server_config in server_manager.servers.items(): for guild_id, server_config in server_manager.servers.items():
@@ -337,7 +340,7 @@ def initialize_v2_system(client):
# Start decay task # Start decay task
client.loop.create_task(periodic_decay_task()) client.loop.create_task(periodic_decay_task())
print("Autonomous V2 System initialized") logger.info("Autonomous V2 System initialized")
# ========== Legacy Function Wrappers ========== # ========== Legacy Function Wrappers ==========

View File

@@ -12,6 +12,9 @@ from typing import Dict, List, Optional
from collections import deque from collections import deque
import discord import discord
from .autonomous_persistence import save_autonomous_context, load_autonomous_context, apply_context_to_signals 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 @dataclass
class ContextSignals: class ContextSignals:
@@ -238,13 +241,13 @@ class AutonomousEngine:
time_since_startup = time.time() - self.bot_startup_time time_since_startup = time.time() - self.bot_startup_time
if time_since_startup < 120: # 2 minutes if time_since_startup < 120: # 2 minutes
if debug: 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 return None
# Never act when asleep # Never act when asleep
if ctx.current_mood == "asleep": if ctx.current_mood == "asleep":
if debug: 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 return None
# Get mood personality # Get mood personality
@@ -254,14 +257,14 @@ class AutonomousEngine:
self._update_activity_metrics(guild_id) self._update_activity_metrics(guild_id)
if debug: if debug:
print(f"\n🔍 [V2 Debug] Decision Check for Guild {guild_id}") logger.debug(f"\n[V2 Debug] Decision Check for Guild {guild_id}")
print(f" Triggered by message: {triggered_by_message}") logger.debug(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})") logger.debug(f" Mood: {ctx.current_mood} (energy={profile['energy']:.2f}, sociability={profile['sociability']:.2f}, impulsiveness={profile['impulsiveness']:.2f})")
print(f" Momentum: {ctx.conversation_momentum:.2f}") logger.debug(f" Momentum: {ctx.conversation_momentum:.2f}")
print(f" Messages (5min/1hr): {ctx.messages_last_5min}/{ctx.messages_last_hour}") logger.debug(f" Messages (5min/1hr): {ctx.messages_last_5min}/{ctx.messages_last_hour}")
print(f" Messages since appearance: {ctx.messages_since_last_appearance}") logger.debug(f" Messages since appearance: {ctx.messages_since_last_appearance}")
print(f" Time since last action: {ctx.time_since_last_action:.0f}s") logger.debug(f" Time since last action: {ctx.time_since_last_action:.0f}s")
print(f" Active activities: {len(ctx.users_started_activity)}") logger.debug(f" Active activities: {len(ctx.users_started_activity)}")
# --- Decision Logic --- # --- Decision Logic ---
@@ -272,7 +275,7 @@ class AutonomousEngine:
# 1. CONVERSATION JOIN (high priority when momentum is high) # 1. CONVERSATION JOIN (high priority when momentum is high)
if self._should_join_conversation(ctx, profile, debug): if self._should_join_conversation(ctx, profile, debug):
if debug: if debug:
print(f"[V2 Debug] DECISION: join_conversation") logger.debug(f"[V2 Debug] DECISION: join_conversation")
return "join_conversation" return "join_conversation"
# 2. USER ENGAGEMENT (someone interesting appeared) # 2. USER ENGAGEMENT (someone interesting appeared)
@@ -280,17 +283,17 @@ class AutonomousEngine:
if triggered_by_message: if triggered_by_message:
# Convert to join_conversation when message-triggered # Convert to join_conversation when message-triggered
if debug: 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" return "join_conversation"
if debug: if debug:
print(f"[V2 Debug] DECISION: engage_user") logger.debug(f"[V2 Debug] DECISION: engage_user")
return "engage_user" return "engage_user"
# 3. FOMO RESPONSE (lots of activity without her) # 3. FOMO RESPONSE (lots of activity without her)
# When FOMO triggers, join the conversation instead of saying something random # When FOMO triggers, join the conversation instead of saying something random
if self._should_respond_to_fomo(ctx, profile, debug): if self._should_respond_to_fomo(ctx, profile, debug):
if 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 return "join_conversation" # Jump in and respond to what's being said
# 4. BORED/LONELY (quiet for too long, depending on mood) # 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 self._should_break_silence(ctx, profile, debug):
if triggered_by_message: if triggered_by_message:
if debug: 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 return "join_conversation" # Respond to the message instead of random general statement
else: else:
if debug: if debug:
print(f"[V2 Debug] DECISION: general (break silence)") logger.debug(f"[V2 Debug] DECISION: general (break silence)")
return "general" return "general"
# 5. SHARE TWEET (low activity, wants to share something) # 5. SHARE TWEET (low activity, wants to share something)
# Skip this entirely when triggered by message - would be inappropriate to ignore user's message # 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 not triggered_by_message and self._should_share_content(ctx, profile, debug):
if debug: if debug:
print(f"[V2 Debug] DECISION: share_tweet") logger.debug(f"[V2 Debug] DECISION: share_tweet")
return "share_tweet" return "share_tweet"
# 6. CHANGE PROFILE PICTURE (very rare, once per day) # 6. CHANGE PROFILE PICTURE (very rare, once per day)
# Skip this entirely when triggered by message # Skip this entirely when triggered by message
if not triggered_by_message and self._should_change_profile_picture(ctx, profile, debug): if not triggered_by_message and self._should_change_profile_picture(ctx, profile, debug):
if debug: if debug:
print(f"[V2 Debug] DECISION: change_profile_picture") logger.debug(f"[V2 Debug] DECISION: change_profile_picture")
return "change_profile_picture" return "change_profile_picture"
if debug: if debug:
print(f"[V2 Debug] DECISION: None (no conditions met)") logger.debug(f"[V2 Debug] DECISION: None (no conditions met)")
return None return None
@@ -341,10 +344,10 @@ class AutonomousEngine:
result = all(conditions.values()) result = all(conditions.values())
if debug: if debug:
print(f" [Join Conv] momentum={ctx.conversation_momentum:.2f} > {mood_adjusted:.2f}? {conditions['momentum_check']}") logger.debug(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']}") logger.debug(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']}") logger.debug(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] impulsive roll? {conditions['impulsiveness_roll']} | Result: {result}")
return result return result
@@ -361,8 +364,8 @@ class AutonomousEngine:
if debug and has_activities: if debug and has_activities:
activities = [name for name, ts in ctx.users_started_activity] 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}") logger.debug(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] roll={roll:.2f} < {threshold:.2f}? {roll_ok} | Result: {result}")
return result return result
@@ -378,9 +381,9 @@ class AutonomousEngine:
result = msgs_check and momentum_check and cooldown_check result = msgs_check and momentum_check and cooldown_check
if debug: if debug:
print(f" [FOMO] messages={ctx.messages_since_last_appearance} > {fomo_threshold:.0f}? {msgs_check}") logger.debug(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}") logger.debug(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] cooldown={ctx.time_since_last_action:.0f}s > 900s? {cooldown_check} | Result: {result}")
return result return result
@@ -397,9 +400,9 @@ class AutonomousEngine:
result = quiet_check and silence_check and energy_ok result = quiet_check and silence_check and energy_ok
if debug: if debug:
print(f" [Silence] msgs_last_hour={ctx.messages_last_hour} < 5? {quiet_check}") logger.debug(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}") logger.debug(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] energy roll={energy_roll:.2f} < {profile['energy']:.2f}? {energy_ok} | Result: {result}")
return result return result
@@ -416,10 +419,10 @@ class AutonomousEngine:
result = quiet_check and cooldown_check and energy_ok and mood_ok result = quiet_check and cooldown_check and energy_ok and mood_ok
if debug: if debug:
print(f" [Share] msgs_last_hour={ctx.messages_last_hour} < 10? {quiet_check}") logger.debug(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}") logger.debug(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}") logger.debug(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] mood '{ctx.current_mood}' appropriate? {mood_ok} | Result: {result}")
return result return result
@@ -447,11 +450,11 @@ class AutonomousEngine:
if hours_since_change < 20: # At least 20 hours between changes if hours_since_change < 20: # At least 20 hours between changes
if debug: 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 return False
except Exception as e: except Exception as e:
if debug: 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) # Only consider changing during certain hours (10 AM - 10 PM)
hour = ctx.hour_of_day hour = ctx.hour_of_day
@@ -472,11 +475,11 @@ class AutonomousEngine:
result = time_check and quiet_check and cooldown_check and roll_ok result = time_check and quiet_check and cooldown_check and roll_ok
if debug: if debug:
print(f" [PFP] hour={hour}, time_ok={time_check}") logger.debug(f" [PFP] hour={hour}, time_ok={time_check}")
print(f" [PFP] msgs_last_hour={ctx.messages_last_hour} < 5? {quiet_check}") logger.debug(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}") logger.debug(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}") logger.debug(f" [PFP] mood_boost={mood_boost}, roll={roll:.4f} < {base_chance:.4f}? {roll_ok}")
print(f" [PFP] Result: {result}") logger.debug(f" [PFP] Result: {result}")
return result return result

View File

@@ -8,6 +8,9 @@ import time
from pathlib import Path from pathlib import Path
from typing import Dict, Optional from typing import Dict, Optional
from datetime import datetime, timezone from datetime import datetime, timezone
from utils.logger import get_logger
logger = get_logger('autonomous')
CONTEXT_FILE = Path("memory/autonomous_context.json") 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) CONTEXT_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(CONTEXT_FILE, 'w') as f: with open(CONTEXT_FILE, 'w') as f:
json.dump(data, f, indent=2) 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: 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]]: 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 - Timestamps are adjusted for elapsed time
""" """
if not CONTEXT_FILE.exists(): if not CONTEXT_FILE.exists():
print(" [V2] No saved context found, starting fresh") logger.info("[V2] No saved context found, starting fresh")
return {}, {} return {}, {}
try: try:
@@ -74,7 +77,7 @@ def load_autonomous_context() -> tuple[Dict[int, dict], Dict[int, float]]:
downtime = time.time() - saved_at downtime = time.time() - saved_at
downtime_minutes = downtime / 60 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 = {} context_data = {}
last_action = {} last_action = {}
@@ -106,13 +109,13 @@ def load_autonomous_context() -> tuple[Dict[int, dict], Dict[int, float]]:
if last_action_timestamp > 0: if last_action_timestamp > 0:
last_action[guild_id] = last_action_timestamp last_action[guild_id] = last_action_timestamp
print(f"[V2] Restored context for {len(context_data)} servers") logger.info(f"[V2] Restored context for {len(context_data)} servers")
print(f" └─ Momentum decay factor: {decay_factor:.3f} (from {downtime_minutes:.1f}min downtime)") logger.debug(f" └─ Momentum decay factor: {decay_factor:.3f} (from {downtime_minutes:.1f}min downtime)")
return context_data, last_action return context_data, last_action
except Exception as e: except Exception as e:
print(f"⚠️ [V2] Failed to load autonomous context: {e}") logger.error(f"[V2] Failed to load autonomous context: {e}")
return {}, {} return {}, {}

View File

@@ -23,6 +23,9 @@ from utils.image_handling import (
convert_gif_to_mp4 convert_gif_to_mp4
) )
from utils.sleep_responses import SLEEP_RESPONSES from utils.sleep_responses import SLEEP_RESPONSES
from utils.logger import get_logger
logger = get_logger('autonomous')
# Server-specific memory storage # Server-specific memory storage
_server_autonomous_messages = {} # guild_id -> rotating buffer of last general messages _server_autonomous_messages = {} # guild_id -> rotating buffer of last general messages
@@ -48,7 +51,7 @@ def save_autonomous_config(config):
def setup_autonomous_speaking(): def setup_autonomous_speaking():
"""Setup autonomous speaking for all configured servers""" """Setup autonomous speaking for all configured servers"""
# This is now handled by the server manager # 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): 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""" """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""" """Miku says something general in a specific server"""
server_config = server_manager.get_server_config(guild_id) server_config = server_manager.get_server_config(guild_id)
if not server_config: if not server_config:
print(f"⚠️ No config found for server {guild_id}") logger.warning(f"No config found for server {guild_id}")
return return
channel = globals.client.get_channel(server_config.autonomous_channel_id) channel = globals.client.get_channel(server_config.autonomous_channel_id)
if not channel: 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 return
# Check if evil mode is active # 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") 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]): if not is_too_similar(message, _server_autonomous_messages[guild_id]):
break break
print("🔁 Response was too similar to past messages, retrying...") logger.debug("Response was too similar to past messages, retrying...")
try: try:
await channel.send(message) 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: if len(_server_autonomous_messages[guild_id]) > MAX_HISTORY:
_server_autonomous_messages[guild_id].pop(0) _server_autonomous_messages[guild_id].pop(0)
character_name = "Evil Miku" if evil_mode else "Miku" 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: 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): 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 """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) server_config = server_manager.get_server_config(guild_id)
if not server_config: if not server_config:
print(f"⚠️ No config found for server {guild_id}") logger.warning(f"No config found for server {guild_id}")
return return
guild = globals.client.get_guild(guild_id) guild = globals.client.get_guild(guild_id)
if not guild: if not guild:
print(f"⚠️ Guild {guild_id} not found.") logger.warning(f"Guild {guild_id} not found.")
return return
channel = globals.client.get_channel(server_config.autonomous_channel_id) channel = globals.client.get_channel(server_config.autonomous_channel_id)
if not channel: 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 return
# Get target user # Get target user
@@ -164,14 +167,14 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
try: try:
target = guild.get_member(int(user_id)) target = guild.get_member(int(user_id))
if not target: 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 return
if target.bot: if target.bot:
print(f"⚠️ Cannot engage bot user {user_id}") logger.warning(f"Cannot engage bot user {user_id}")
return 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: except ValueError:
print(f"⚠️ Invalid user ID: {user_id}") logger.warning(f"Invalid user ID: {user_id}")
return return
else: else:
# Pick random user # 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: 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 return
target = random.choice(members) 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() 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() now = time.time()
last_time = _server_user_engagements[guild_id].get(target.id, 0) last_time = _server_user_engagements[guild_id].get(target.id, 0)
if now - last_time < 43200: # 12 hours in seconds 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) await miku_say_something_general_for_server(guild_id)
return return
@@ -286,7 +289,7 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
) )
if engagement_type: if engagement_type:
print(f"💬 Engagement type: {engagement_type}") logger.debug(f"Engagement type: {engagement_type}")
try: try:
# Use consistent user_id for engaging users to enable conversation history # 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}") await channel.send(f"{target.mention} {message}")
_server_user_engagements[guild_id][target.id] = time.time() _server_user_engagements[guild_id][target.id] = time.time()
character_name = "Evil Miku" if evil_mode else "Miku" 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: 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): async def miku_detect_and_join_conversation_for_server(guild_id: int, force: bool = False):
"""Miku detects and joins conversations in a specific server """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 guild_id: The server ID
force: If True, bypass activity checks and random chance (for manual triggers) 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) server_config = server_manager.get_server_config(guild_id)
if not server_config: if not server_config:
print(f"⚠️ No config found for server {guild_id}") logger.warning(f"No config found for server {guild_id}")
return return
channel = globals.client.get_channel(server_config.autonomous_channel_id) channel = globals.client.get_channel(server_config.autonomous_channel_id)
if not isinstance(channel, TextChannel): 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 return
# Fetch last 20 messages (for filtering) # Fetch last 20 messages (for filtering)
try: try:
messages = [msg async for msg in channel.history(limit=20)] 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: 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 return
# Filter messages based on force mode # Filter messages based on force mode
if force: if force:
# When forced, use messages from real users (no time limit) - but limit to last 10 # 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] 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: else:
# Normal mode: Filter to messages in last 10 minutes from real users (not bots) # Normal mode: Filter to messages in last 10 minutes from real users (not bots)
recent_msgs = [ 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 if not msg.author.bot
and (datetime.now(msg.created_at.tzinfo) - msg.created_at).total_seconds() < 600 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) user_ids = set(msg.author.id for msg in recent_msgs)
if not force: if not force:
if len(recent_msgs) < 5 or len(user_ids) < 2: if len(recent_msgs) < 5 or len(user_ids) < 2:
# Not enough activity # 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 return
if random.random() > 0.5: 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 return # 50% chance to engage
else: 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: 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 return
# Use last 10 messages for context (oldest to newest) # 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") reply = await query_llama(prompt, user_id=f"miku-conversation-{guild_id}", guild_id=guild_id, response_type="conversation_join")
await channel.send(reply) await channel.send(reply)
character_name = "Evil Miku" if evil_mode else "Miku" 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: 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): async def share_miku_tweet_for_server(guild_id: int):
"""Share a Miku tweet in a specific server""" """Share a Miku tweet in a specific server"""
server_config = server_manager.get_server_config(guild_id) server_config = server_manager.get_server_config(guild_id)
if not server_config: if not server_config:
print(f"⚠️ No config found for server {guild_id}") logger.warning(f"No config found for server {guild_id}")
return return
channel = globals.client.get_channel(server_config.autonomous_channel_id) channel = globals.client.get_channel(server_config.autonomous_channel_id)
tweets = await fetch_miku_tweets(limit=5) tweets = await fetch_miku_tweets(limit=5)
if not tweets: 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 return
fresh_tweets = [t for t in tweets if t["url"] not in LAST_SENT_TWEETS] fresh_tweets = [t for t in tweets if t["url"] not in LAST_SENT_TWEETS]
if not fresh_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 fresh_tweets = tweets
tweet = random.choice(fresh_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""" """Handle custom prompt for a specific server"""
server_config = server_manager.get_server_config(guild_id) server_config = server_manager.get_server_config(guild_id)
if not server_config: 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 return False
channel = globals.client.get_channel(server_config.autonomous_channel_id) channel = globals.client.get_channel(server_config.autonomous_channel_id)
if not channel: 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 return False
mood = server_config.current_mood_name 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 # 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") message = await query_llama(prompt, user_id=f"miku-manual-{guild_id}", guild_id=guild_id, response_type="autonomous_general")
await channel.send(message) 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 # Add to server-specific message history
if guild_id not in _server_autonomous_messages: 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 return True
except Exception as e: 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 return False
# Legacy functions for backward compatibility - these now delegate to server-specific versions # 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: with open(LAST_SENT_TWEETS_FILE, "r", encoding="utf-8") as f:
LAST_SENT_TWEETS = json.load(f) LAST_SENT_TWEETS = json.load(f)
except Exception as e: 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 = [] LAST_SENT_TWEETS = []
else: else:
LAST_SENT_TWEETS = [] LAST_SENT_TWEETS = []
@@ -552,7 +555,7 @@ def save_last_sent_tweets():
with open(LAST_SENT_TWEETS_FILE, "w", encoding="utf-8") as f: with open(LAST_SENT_TWEETS_FILE, "w", encoding="utf-8") as f:
json.dump(LAST_SENT_TWEETS, f) json.dump(LAST_SENT_TWEETS, f)
except Exception as e: 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(): def get_time_of_day():
hour = datetime.now().hour + 3 hour = datetime.now().hour + 3
@@ -602,7 +605,7 @@ async def _analyze_message_media(message):
try: try:
# Handle images # Handle images
if any(attachment.filename.lower().endswith(ext) for ext in [".jpg", ".jpeg", ".png", ".webp"]): 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) base64_img = await download_and_encode_image(attachment.url)
if base64_img: if base64_img:
description = await analyze_image_with_qwen(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"]): elif any(attachment.filename.lower().endswith(ext) for ext in [".gif", ".mp4", ".webm", ".mov"]):
is_gif = attachment.filename.lower().endswith('.gif') is_gif = attachment.filename.lower().endswith('.gif')
media_type = "GIF" if is_gif else "video" 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 # Download media
media_bytes_b64 = await download_and_encode_media(attachment.url) 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}]" return f"[{media_type}: {description}]"
except Exception as e: except Exception as e:
print(f" ⚠️ Error analyzing media for reaction: {e}") logger.warning(f" Error analyzing media for reaction: {e}")
continue continue
return None 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) # 50% chance to proceed (unless forced or with a specific message)
if not force and force_message is None and random.random() > 0.5: 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 return
server_config = server_manager.get_server_config(guild_id) server_config = server_manager.get_server_config(guild_id)
if not server_config: if not server_config:
print(f"⚠️ No config found for server {guild_id}") logger.warning(f"No config found for server {guild_id}")
return return
server_name = server_config.guild_name server_name = server_config.guild_name
# Don't react if asleep # Don't react if asleep
if server_config.current_mood_name == "asleep" or server_config.is_sleeping: 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 return
# Get the autonomous channel # Get the autonomous channel
channel = globals.client.get_channel(server_config.autonomous_channel_id) channel = globals.client.get_channel(server_config.autonomous_channel_id)
if not channel: if not channel:
print(f"⚠️ [{server_name}] Autonomous channel not found") logger.warning(f"[{server_name}] Autonomous channel not found")
return return
try: try:
@@ -677,9 +680,9 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
target_message = force_message target_message = force_message
# Check if we've already reacted to this message # Check if we've already reacted to this message
if target_message.id in _reacted_message_ids: 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 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: else:
# Fetch recent messages (last 50 messages to get more candidates) # Fetch recent messages (last 50 messages to get more candidates)
messages = [] messages = []
@@ -697,14 +700,14 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
messages.append(message) messages.append(message)
if not messages: 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 return
# Pick a random message from the recent ones # Pick a random message from the recent ones
target_message = random.choice(messages) target_message = random.choice(messages)
# Analyze any media in the message # 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) media_description = await _analyze_message_media(target_message)
# Build message content with media description if present # 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] emoji = emojis[0]
else: else:
# No emoji found in response, use fallback # 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 = "💙" emoji = "💙"
# Final validation: try adding the reaction # 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) await target_message.add_reaction(emoji)
except discord.HTTPException as e: except discord.HTTPException as e:
if "Unknown Emoji" in str(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 = "💙" emoji = "💙"
await target_message.add_reaction(emoji) await target_message.add_reaction(emoji)
else: 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: for msg_id in ids_to_remove:
_reacted_message_ids.discard(msg_id) _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: 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: 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: 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): async def miku_autonomous_reaction(force=False):
"""Legacy function - run autonomous reactions for all servers """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) # 50% chance to proceed (unless forced with a specific message)
if force_message is None and random.random() > 0.5: 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 return
# Get the user object # Get the user object
try: try:
user = await globals.client.fetch_user(user_id) user = await globals.client.fetch_user(user_id)
if not user: if not user:
print(f"⚠️ Could not find user {user_id}") logger.warning(f"Could not find user {user_id}")
return return
dm_channel = user.dm_channel 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 username = user.display_name
except Exception as e: 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 return
try: try:
@@ -842,9 +845,9 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
target_message = force_message target_message = force_message
# Check if we've already reacted to this message # Check if we've already reacted to this message
if target_message.id in _reacted_message_ids: 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 return
print(f"🎯 [DM: {username}] Reacting to new message") logger.info(f"[DM: {username}] Reacting to new message")
else: else:
# Fetch recent messages from DM (last 50 messages) # Fetch recent messages from DM (last 50 messages)
messages = [] messages = []
@@ -862,14 +865,14 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
messages.append(message) messages.append(message)
if not messages: 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 return
# Pick a random message from the recent ones # Pick a random message from the recent ones
target_message = random.choice(messages) target_message = random.choice(messages)
# Analyze any media in the message # 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) media_description = await _analyze_message_media(target_message)
# Build message content with media description if present # 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] emoji = emojis[0]
else: else:
# No emoji found in response, use fallback # 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 = "💙" emoji = "💙"
# Final validation: try adding the reaction # 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) await target_message.add_reaction(emoji)
except discord.HTTPException as e: except discord.HTTPException as e:
if "Unknown Emoji" in str(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 = "💙" emoji = "💙"
await target_message.add_reaction(emoji) await target_message.add_reaction(emoji)
else: 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: for msg_id in ids_to_remove:
_reacted_message_ids.discard(msg_id) _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: 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: 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: 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): 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 # Check if enough time has passed
if not should_update_profile_picture(): 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 return
# Get server config to use current mood # Get server config to use current mood
server_config = server_manager.get_server_config(guild_id) server_config = server_manager.get_server_config(guild_id)
if not server_config: if not server_config:
print(f"⚠️ No config found for server {guild_id}") logger.warning(f"No config found for server {guild_id}")
return return
mood = server_config.current_mood_name 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: try:
success = await update_profile_picture(globals.client, mood=mood) 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! ✨" "*updates avatar* Time for a fresh look! ✨"
] ]
await channel.send(random.choice(messages)) 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: else:
print(f"⚠️ [Server: {guild_id}] Profile picture update failed") logger.warning(f"[Server: {guild_id}] Profile picture update failed")
except Exception as e: 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}")

View File

@@ -11,6 +11,9 @@ import random
import asyncio import asyncio
import discord import discord
import globals import globals
from utils.logger import get_logger
logger = get_logger('persona')
# ============================================================================ # ============================================================================
# CONSTANTS # CONSTANTS
@@ -38,26 +41,26 @@ def save_bipolar_state():
} }
with open(BIPOLAR_STATE_FILE, "w", encoding="utf-8") as f: with open(BIPOLAR_STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2) 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: 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(): def load_bipolar_state():
"""Load bipolar mode state from JSON file""" """Load bipolar mode state from JSON file"""
try: try:
if not os.path.exists(BIPOLAR_STATE_FILE): 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 return False
with open(BIPOLAR_STATE_FILE, "r", encoding="utf-8") as f: with open(BIPOLAR_STATE_FILE, "r", encoding="utf-8") as f:
state = json.load(f) state = json.load(f)
bipolar_mode = state.get("bipolar_mode_enabled", False) 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 return bipolar_mode
except Exception as e: 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 return False
@@ -71,16 +74,16 @@ def save_webhooks():
with open(BIPOLAR_WEBHOOKS_FILE, "w", encoding="utf-8") as f: with open(BIPOLAR_WEBHOOKS_FILE, "w", encoding="utf-8") as f:
json.dump(webhooks_data, f, indent=2) 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: except Exception as e:
print(f"⚠️ Failed to save bipolar webhooks: {e}") logger.error(f"Failed to save bipolar webhooks: {e}")
def load_webhooks(): def load_webhooks():
"""Load webhook URLs from JSON file""" """Load webhook URLs from JSON file"""
try: try:
if not os.path.exists(BIPOLAR_WEBHOOKS_FILE): if not os.path.exists(BIPOLAR_WEBHOOKS_FILE):
print(" No bipolar webhooks file found") logger.info("No bipolar webhooks file found")
return {} return {}
with open(BIPOLAR_WEBHOOKS_FILE, "r", encoding="utf-8") as f: 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(): for guild_id_str, webhook_data in webhooks_data.items():
webhooks[int(guild_id_str)] = webhook_data 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 return webhooks
except Exception as e: except Exception as e:
print(f"⚠️ Failed to load bipolar webhooks: {e}") logger.error(f"Failed to load bipolar webhooks: {e}")
return {} return {}
@@ -105,8 +108,8 @@ def restore_bipolar_mode_on_startup():
globals.BIPOLAR_WEBHOOKS = load_webhooks() globals.BIPOLAR_WEBHOOKS = load_webhooks()
if bipolar_mode: if bipolar_mode:
print("🔄 Bipolar mode restored from previous session") logger.info("Bipolar mode restored from previous session")
print("💬 Persona dialogue system enabled (natural conversations + arguments)") logger.info("Persona dialogue system enabled (natural conversations + arguments)")
return bipolar_mode return bipolar_mode
@@ -124,7 +127,7 @@ def load_scoreboard() -> dict:
with open(BIPOLAR_SCOREBOARD_FILE, "r", encoding="utf-8") as f: with open(BIPOLAR_SCOREBOARD_FILE, "r", encoding="utf-8") as f:
return json.load(f) return json.load(f)
except Exception as e: 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": []} 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) os.makedirs(os.path.dirname(BIPOLAR_SCOREBOARD_FILE), exist_ok=True)
with open(BIPOLAR_SCOREBOARD_FILE, "w", encoding="utf-8") as f: with open(BIPOLAR_SCOREBOARD_FILE, "w", encoding="utf-8") as f:
json.dump(scoreboard, f, indent=2) 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: 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 = ""): def record_argument_result(winner: str, exchanges: int, reasoning: str = ""):
@@ -205,7 +208,7 @@ def enable_bipolar_mode():
"""Enable bipolar mode""" """Enable bipolar mode"""
globals.BIPOLAR_MODE = True globals.BIPOLAR_MODE = True
save_bipolar_state() save_bipolar_state()
print("🔄 Bipolar mode enabled!") logger.info("Bipolar mode enabled!")
def disable_bipolar_mode(): def disable_bipolar_mode():
@@ -214,7 +217,7 @@ def disable_bipolar_mode():
# Clear any ongoing arguments # Clear any ongoing arguments
globals.BIPOLAR_ARGUMENT_IN_PROGRESS.clear() globals.BIPOLAR_ARGUMENT_IN_PROGRESS.clear()
save_bipolar_state() save_bipolar_state()
print("🔄 Bipolar mode disabled!") logger.info("Bipolar mode disabled!")
def toggle_bipolar_mode() -> bool: 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: if miku_webhook and evil_webhook:
return {"miku": miku_webhook, "evil_miku": evil_webhook} return {"miku": miku_webhook, "evil_miku": evil_webhook}
except Exception as e: except Exception as e:
print(f"⚠️ Failed to retrieve cached webhooks: {e}") logger.warning(f"Failed to retrieve cached webhooks: {e}")
# Create new webhooks # Create new webhooks
try: try:
print(f"🔧 Creating bipolar webhooks for channel #{channel.name}") logger.info(f"Creating bipolar webhooks for channel #{channel.name}")
# Load avatar images # Load avatar images
miku_avatar = None miku_avatar = None
@@ -300,14 +303,14 @@ async def get_or_create_webhooks_for_channel(channel: discord.TextChannel) -> di
} }
save_webhooks() 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} return {"miku": miku_webhook, "evil_miku": evil_webhook}
except discord.Forbidden: 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 return None
except Exception as e: except Exception as e:
print(f"Failed to create webhooks: {e}") logger.error(f"Failed to create webhooks: {e}")
return None return None
@@ -322,11 +325,11 @@ async def cleanup_webhooks(client):
await webhook.delete(reason="Bipolar mode cleanup") await webhook.delete(reason="Bipolar mode cleanup")
cleaned_count += 1 cleaned_count += 1
except Exception as e: 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() globals.BIPOLAR_WEBHOOKS.clear()
save_webhooks() save_webhooks()
print(f"🧹 Cleaned up {cleaned_count} bipolar webhook(s)") logger.info(f"Cleaned up {cleaned_count} bipolar webhook(s)")
return cleaned_count 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"): 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." return "draw", "The arbiter could not make a decision."
# Parse the judgment - look at the first line/sentence for the 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 = judgment_lines[0].strip().strip('"').strip()
first_line_lower = first_line.lower() 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 # Check the first line for the decision - be very specific
# The arbiter should respond with ONLY the name on the first line # The arbiter should respond with ONLY the name on the first line
if first_line_lower == "evil miku": if first_line_lower == "evil miku":
winner = "evil" 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": elif first_line_lower == "hatsune miku":
winner = "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": elif first_line_lower == "draw":
winner = "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: elif "evil miku" in first_line_lower and "hatsune" not in first_line_lower:
# First line mentions Evil Miku but not Hatsune Miku # First line mentions Evil Miku but not Hatsune Miku
winner = "evil" 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: elif "hatsune miku" in first_line_lower and "evil" not in first_line_lower:
# First line mentions Hatsune Miku but not Evil Miku # First line mentions Hatsune Miku but not Evil Miku
winner = "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: else:
# Fallback: check the whole judgment # 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() judgment_lower = judgment.lower()
# Count mentions to break ties # Count mentions to break ties
evil_count = judgment_lower.count("evil miku") evil_count = judgment_lower.count("evil miku")
miku_count = judgment_lower.count("hatsune miku") miku_count = judgment_lower.count("hatsune miku")
draw_count = judgment_lower.count("draw") 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: if draw_count > 0 and draw_count >= evil_count and draw_count >= miku_count:
winner = "draw" winner = "draw"
@@ -654,7 +657,7 @@ async def judge_argument_winner(conversation_log: list, guild_id: int) -> tuple[
return winner, judgment return winner, judgment
except Exception as e: 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." 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 guild_id = channel.guild.id
if is_argument_in_progress(channel_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 return
# Get webhooks for this channel # Get webhooks for this channel
webhooks = await get_or_create_webhooks_for_channel(channel) webhooks = await get_or_create_webhooks_for_channel(channel)
if not webhooks: 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 return
# Determine who initiates based on starting_message or inactive persona # 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 "")) 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 initiator = "miku" if is_evil_message else "evil" # Opposite persona responds
last_message = starting_message.content last_message = starting_message.content
print(f"🔄 Starting argument from message, responder: {initiator}") logger.info(f"Starting argument from message, responder: {initiator}")
else: else:
# The inactive persona breaks through # The inactive persona breaks through
initiator = get_inactive_persona() initiator = get_inactive_persona()
last_message = None 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) 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 globals.EVIL_MODE = original_evil_mode
if not initial_message or initial_message.startswith("Error") or initial_message.startswith("Sorry"): 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) end_argument(channel_id)
return return
@@ -877,22 +880,22 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
if should_end: if should_end:
exchange_count = globals.BIPOLAR_ARGUMENT_IN_PROGRESS.get(channel_id, {}).get("exchange_count", 0) 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 # Use arbiter to judge the winner
winner, judgment = await judge_argument_winner(conversation_log, guild_id) winner, judgment = await judge_argument_winner(conversation_log, guild_id)
print(f"⚖️ Arbiter decision: {winner}") logger.info(f"Arbiter decision: {winner}")
print(f"📝 Judgment: {judgment}") logger.info(f"Judgment: {judgment}")
# If it's a draw, continue the argument instead of ending # If it's a draw, continue the argument instead of ending
if winner == "draw": 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%) # 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) 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) new_end_chance = max(0.05, current_end_chance - 0.05)
globals.BIPOLAR_ARGUMENT_IN_PROGRESS[channel_id]["end_chance"] = new_end_chance 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 # Don't end, just continue to the next exchange
else: else:
# Clear winner - generate final triumphant message # 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) # Switch to winner's mode (including role color)
from utils.evil_mode import apply_evil_mode_changes, revert_evil_mode_changes from utils.evil_mode import apply_evil_mode_changes, revert_evil_mode_changes
if winner == "evil": 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) await apply_evil_mode_changes(client, change_username=True, change_pfp=True, change_nicknames=True, change_role_color=True)
else: 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) await revert_evil_mode_changes(client, change_username=True, change_pfp=True, change_nicknames=True, change_role_color=True)
# Clean up argument conversation history # 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 pass # History cleanup is not critical
end_argument(channel_id) end_argument(channel_id)
print(f"Argument ended in #{channel.name}, winner: {winner}") logger.info(f"Argument ended in #{channel.name}, winner: {winner}")
return return
# Get current speaker # Get current speaker
@@ -982,7 +985,7 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
globals.EVIL_MODE = original_evil_mode globals.EVIL_MODE = original_evil_mode
if not response or response.startswith("Error") or response.startswith("Sorry"): 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) end_argument(channel_id)
return return
@@ -1021,7 +1024,7 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
is_first_response = False is_first_response = False
except Exception as e: except Exception as e:
print(f"Argument error: {e}") logger.error(f"Argument error: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
end_argument(channel_id) 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 starting_message: Optional message to use as the first message in the argument
""" """
if not globals.BIPOLAR_MODE: 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 return False
if is_argument_in_progress(channel.id): 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 return False
asyncio.create_task(run_argument(channel, client, context, starting_message)) asyncio.create_task(run_argument(channel, client, context, starting_message))

View File

@@ -5,13 +5,18 @@ Replaces the vector search system with organized, complete context.
Preserves original content files in their entirety. Preserves original content files in their entirety.
""" """
from utils.logger import get_logger
logger = get_logger('core')
def get_original_miku_lore() -> str: def get_original_miku_lore() -> str:
"""Load the complete, unmodified miku_lore.txt file""" """Load the complete, unmodified miku_lore.txt file"""
try: try:
with open("miku_lore.txt", "r", encoding="utf-8") as f: with open("miku_lore.txt", "r", encoding="utf-8") as f:
return f.read() return f.read()
except Exception as e: 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]" 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: with open("miku_prompt.txt", "r", encoding="utf-8") as f:
return f.read() return f.read()
except Exception as e: 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]" 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: with open("miku_lyrics.txt", "r", encoding="utf-8") as f:
return f.read() return f.read()
except Exception as e: 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]" return "## MIKU LYRICS\n[File could not be loaded]"

View File

@@ -8,6 +8,9 @@ import globals
from langchain_community.vectorstores import FAISS from langchain_community.vectorstores import FAISS
from langchain_text_splitters import CharacterTextSplitter, RecursiveCharacterTextSplitter from langchain_text_splitters import CharacterTextSplitter, RecursiveCharacterTextSplitter
from langchain_core.documents import Document 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 # 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 # Safety check: ensure guild and guild.me exist
if not message.guild or not message.guild.me: 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 return False
# If message contains a ping for Miku, return true # 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: if referenced_msg.author == message.guild.me:
return True return True
except Exception as e: 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() cleaned = message.content.strip()

View File

@@ -7,6 +7,10 @@ import aiohttp
import random import random
from typing import Optional, List, Dict from typing import Optional, List, Dict
import asyncio import asyncio
from utils.logger import get_logger
logger = get_logger('media')
class DanbooruClient: class DanbooruClient:
"""Client for interacting with Danbooru API""" """Client for interacting with Danbooru API"""
@@ -74,23 +78,23 @@ class DanbooruClient:
try: try:
url = f"{self.BASE_URL}/posts.json" 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: async with self.session.get(url, params=params, timeout=10) as response:
if response.status == 200: if response.status == 200:
posts = await response.json() 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 return posts
else: else:
error_text = await response.text() error_text = await response.text()
print(f"⚠️ Danbooru API error: {response.status}") logger.error(f"Danbooru API error: {response.status}")
print(f"⚠️ Request URL: {response.url}") logger.error(f"Request URL: {response.url}")
print(f"⚠️ Error details: {error_text[:500]}") logger.error(f"Error details: {error_text[:500]}")
return [] return []
except asyncio.TimeoutError: except asyncio.TimeoutError:
print(f"⚠️ Danbooru API timeout") logger.error(f"Danbooru API timeout")
return [] return []
except Exception as e: except Exception as e:
print(f"⚠️ Danbooru API error: {e}") logger.error(f"Danbooru API error: {e}")
return [] return []
async def get_random_miku_image( async def get_random_miku_image(
@@ -128,7 +132,7 @@ class DanbooruClient:
) )
if not posts: 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 # Fallback: try without mood tags
posts = await self.search_miku_images( posts = await self.search_miku_images(
rating=["g", "s"], rating=["g", "s"],
@@ -146,13 +150,13 @@ class DanbooruClient:
] ]
if not valid_posts: if not valid_posts:
print("⚠️ No valid posts with sufficient resolution") logger.warning("No valid posts with sufficient resolution")
return None return None
# Pick a random one # Pick a random one
selected = random.choice(valid_posts) 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 return selected

View File

@@ -11,6 +11,9 @@ import discord
import globals import globals
from utils.llm import query_llama from utils.llm import query_llama
from utils.dm_logger import dm_logger from utils.dm_logger import dm_logger
from utils.logger import get_logger
logger = get_logger('dm')
# Directories # Directories
REPORTS_DIR = "memory/dm_reports" REPORTS_DIR = "memory/dm_reports"
@@ -26,7 +29,7 @@ class DMInteractionAnalyzer:
""" """
self.owner_user_id = owner_user_id self.owner_user_id = owner_user_id
os.makedirs(REPORTS_DIR, exist_ok=True) 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]: def _load_reported_today(self) -> Dict[str, str]:
"""Load the list of users reported today with their dates""" """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: with open(REPORTED_TODAY_FILE, 'r', encoding='utf-8') as f:
return json.load(f) return json.load(f)
except Exception as e: 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 {}
return {} return {}
@@ -45,7 +48,7 @@ class DMInteractionAnalyzer:
with open(REPORTED_TODAY_FILE, 'w', encoding='utf-8') as f: with open(REPORTED_TODAY_FILE, 'w', encoding='utf-8') as f:
json.dump(reported, f, indent=2) json.dump(reported, f, indent=2)
except Exception as e: 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]: def _clean_old_reports(self, reported: Dict[str, str]) -> Dict[str, str]:
"""Remove entries from reported_today that are older than 24 hours""" """Remove entries from reported_today that are older than 24 hours"""
@@ -58,7 +61,7 @@ class DMInteractionAnalyzer:
if now - report_date < timedelta(hours=24): if now - report_date < timedelta(hours=24):
cleaned[user_id] = date_str cleaned[user_id] = date_str
except Exception as e: 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 return cleaned
@@ -91,7 +94,7 @@ class DMInteractionAnalyzer:
if msg_time >= cutoff_time: if msg_time >= cutoff_time:
recent_messages.append(msg) recent_messages.append(msg)
except Exception as e: except Exception as e:
print(f"⚠️ Failed to parse message timestamp: {e}") logger.error(f"Failed to parse message timestamp: {e}")
return recent_messages return recent_messages
@@ -126,14 +129,14 @@ class DMInteractionAnalyzer:
recent_messages = self._get_recent_messages(user_id, hours=24) recent_messages = self._get_recent_messages(user_id, hours=24)
if not recent_messages: 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 return None
# Count user messages only (not bot responses) # Count user messages only (not bot responses)
user_messages = [msg for msg in recent_messages if not msg.get("is_bot_message", False)] 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 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 return None
# Format messages for analysis # Format messages for analysis
@@ -174,7 +177,7 @@ Respond ONLY with the JSON object, no other text."""
response_type="dm_analysis" 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 # Parse JSON response
# Remove markdown code blocks if present # 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: if start_idx != -1 and end_idx != -1:
cleaned_response = cleaned_response[start_idx: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) analysis = json.loads(cleaned_response)
@@ -205,11 +208,11 @@ Respond ONLY with the JSON object, no other text."""
return analysis return analysis
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
print(f"⚠️ JSON parse error for user {username}: {e}") logger.error(f"JSON parse error for user {username}: {e}")
print(f"⚠️ Failed response: {response}") logger.error(f"Failed response: {response}")
return None return None
except Exception as e: 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 return None
def _save_report(self, user_id: int, analysis: Dict) -> str: def _save_report(self, user_id: int, analysis: Dict) -> str:
@@ -221,10 +224,10 @@ Respond ONLY with the JSON object, no other text."""
try: try:
with open(filepath, 'w', encoding='utf-8') as f: with open(filepath, 'w', encoding='utf-8') as f:
json.dump(analysis, f, indent=2, ensure_ascii=False) json.dump(analysis, f, indent=2, ensure_ascii=False)
print(f"💾 Saved report: {filepath}") logger.info(f"Saved report: {filepath}")
return filepath return filepath
except Exception as e: except Exception as e:
print(f"⚠️ Failed to save report: {e}") logger.error(f"Failed to save report: {e}")
return "" return ""
async def _send_report_to_owner(self, analysis: Dict): async def _send_report_to_owner(self, analysis: Dict):
@@ -232,7 +235,7 @@ Respond ONLY with the JSON object, no other text."""
try: try:
# Ensure we're using the Discord client's event loop # Ensure we're using the Discord client's event loop
if not globals.client or not globals.client.is_ready(): 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 return
owner = await globals.client.fetch_user(self.owner_user_id) 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) 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: 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: async def analyze_and_report(self, user_id: int) -> bool:
""" """
@@ -306,12 +309,11 @@ Respond ONLY with the JSON object, no other text."""
Returns: Returns:
True if analysis was performed and reported, False otherwise True if analysis was performed and reported, False otherwise
""" """
# Check if already reported today # Check if already reported today
if self.has_been_reported_today(user_id): if self.has_been_reported_today(user_id):
print(f"📊 User {user_id} already reported today, skipping") logger.debug(f"User {user_id} already reported today, skipping")
return False return False # Analyze interaction
# Analyze interaction
analysis = await self.analyze_user_interaction(user_id) analysis = await self.analyze_user_interaction(user_id)
if not analysis: if not analysis:
@@ -331,13 +333,13 @@ Respond ONLY with the JSON object, no other text."""
async def run_daily_analysis(self): async def run_daily_analysis(self):
"""Run analysis on all DM users and report significant interactions""" """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 # Get all DM users
all_users = dm_logger.get_all_dm_users() all_users = dm_logger.get_all_dm_users()
if not all_users: if not all_users:
print("📊 No DM users to analyze") logger.info("No DM users to analyze")
return return
reported_count = 0 reported_count = 0
@@ -363,9 +365,9 @@ Respond ONLY with the JSON object, no other text."""
analyzed_count += 1 analyzed_count += 1
except Exception as e: 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) # Global instance (will be initialized with owner ID)

View File

@@ -9,6 +9,9 @@ import discord
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Optional
import globals import globals
from utils.logger import get_logger
logger = get_logger('dm')
# Directory for storing DM logs # Directory for storing DM logs
DM_LOG_DIR = "memory/dms" DM_LOG_DIR = "memory/dms"
@@ -19,7 +22,7 @@ class DMLogger:
"""Initialize the DM logger and ensure directory exists""" """Initialize the DM logger and ensure directory exists"""
os.makedirs(DM_LOG_DIR, exist_ok=True) os.makedirs(DM_LOG_DIR, exist_ok=True)
os.makedirs("memory", 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: def _get_user_log_file(self, user_id: int) -> str:
"""Get the log file path for a specific user""" """Get the log file path for a specific user"""
@@ -28,19 +31,19 @@ class DMLogger:
def _load_user_logs(self, user_id: int) -> dict: def _load_user_logs(self, user_id: int) -> dict:
"""Load existing logs for a user, create new if doesn't exist""" """Load existing logs for a user, create new if doesn't exist"""
log_file = self._get_user_log_file(user_id) 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): if os.path.exists(log_file):
try: try:
with open(log_file, 'r', encoding='utf-8') as f: with open(log_file, 'r', encoding='utf-8') as f:
logs = json.load(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 return logs
except Exception as e: 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": []} return {"user_id": user_id, "username": "Unknown", "conversations": []}
else: 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": []} return {"user_id": user_id, "username": "Unknown", "conversations": []}
def _save_user_logs(self, user_id: int, logs: dict): 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: with open(log_file, 'w', encoding='utf-8') as f:
json.dump(logs, f, indent=2, ensure_ascii=False) json.dump(logs, f, indent=2, ensure_ascii=False)
except Exception as e: 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): def log_user_message(self, user: discord.User, message: discord.Message, is_bot_message: bool = False):
"""Log a user message in DMs""" """Log a user message in DMs"""
@@ -92,15 +95,15 @@ class DMLogger:
# Keep only last 1000 messages to prevent files from getting too large # Keep only last 1000 messages to prevent files from getting too large
if len(logs["conversations"]) > 1000: if len(logs["conversations"]) > 1000:
logs["conversations"] = 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 # Save logs
self._save_user_logs(user_id, logs) self._save_user_logs(user_id, logs)
if is_bot_message: 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: 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: def get_user_conversation_summary(self, user_id: int) -> dict:
"""Get a summary of conversations with a user""" """Get a summary of conversations with a user"""
@@ -211,10 +214,10 @@ class DMLogger:
bot_msg = MockMessage(bot_response, attachments=bot_attachments) bot_msg = MockMessage(bot_response, attachments=bot_attachments)
self.log_user_message(user, bot_msg, is_bot_message=True) 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: 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: def export_user_conversation(self, user_id: int, format: str = "json") -> str:
"""Export all conversations with a user in specified format""" """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: with open(BLOCKED_USERS_FILE, 'r', encoding='utf-8') as f:
return json.load(f) return json.load(f)
except Exception as e: 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": []}
return {"blocked_users": []} return {"blocked_users": []}
@@ -262,9 +265,9 @@ class DMLogger:
"""Save the blocked users list""" """Save the blocked users list"""
try: try:
with open(BLOCKED_USERS_FILE, 'w', encoding='utf-8') as f: 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: 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: def is_user_blocked(self, user_id: int) -> bool:
"""Check if a user is blocked""" """Check if a user is blocked"""
@@ -289,13 +292,13 @@ class DMLogger:
} }
self._save_blocked_users(blocked_data) 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 return True
else: else:
print(f"⚠️ User {user_id} is already blocked") logger.warning(f"User {user_id} is already blocked")
return False return False
except Exception as e: 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 return False
def unblock_user(self, user_id: int) -> bool: def unblock_user(self, user_id: int) -> bool:
@@ -313,13 +316,13 @@ class DMLogger:
username = "Unknown" username = "Unknown"
self._save_blocked_users(blocked_data) 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 return True
else: else:
print(f"⚠️ User {user_id} is not blocked") logger.warning(f"User {user_id} is not blocked")
return False return False
except Exception as e: 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 return False
def get_blocked_users(self) -> List[dict]: def get_blocked_users(self) -> List[dict]:
@@ -368,17 +371,17 @@ class DMLogger:
self._save_user_logs(user_id, logs) self._save_user_logs(user_id, logs)
reactor_type = "🤖 Miku" if is_bot_reactor else f"👤 {reactor_name}" 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 return True
else: 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 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 return False
except Exception as e: 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 return False
async def log_reaction_remove(self, user_id: int, message_id: int, emoji: str, reactor_id: int): 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: if len(message["reactions"]) < original_count:
self._save_user_logs(user_id, logs) 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 return True
else: 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 return False
else: else:
print(f"⚠️ No reactions on message {message_id}") logger.debug(f"No reactions on message {message_id}")
return False 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 return False
except Exception as e: 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 return False
async def delete_conversation(self, user_id: int, conversation_id: str) -> bool: async def delete_conversation(self, user_id: int, conversation_id: str) -> bool:
@@ -420,8 +423,8 @@ class DMLogger:
try: try:
logs = self._load_user_logs(user_id) logs = self._load_user_logs(user_id)
print(f"🔍 DM Logger: Looking for bot message ID '{conversation_id}' for user {user_id}") logger.debug(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: Searching through {len(logs['conversations'])} conversations")
# Convert conversation_id to int for comparison if it looks like a Discord message ID # Convert conversation_id to int for comparison if it looks like a Discord message ID
conv_id_as_int = None conv_id_as_int = None
@@ -441,7 +444,7 @@ class DMLogger:
break break
if not message_to_delete: 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 return False
# Try to delete from Discord first # Try to delete from Discord first
@@ -463,13 +466,13 @@ class DMLogger:
discord_message = await dm_channel.fetch_message(int(message_id)) discord_message = await dm_channel.fetch_message(int(message_id))
await discord_message.delete() await discord_message.delete()
discord_deleted = True 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: 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 # Continue anyway to delete from logs
except Exception as e: except Exception as e:
print(f"⚠️ Discord deletion failed: {e}") logger.warning(f"Discord deletion failed: {e}")
# Continue anyway to delete from logs # Continue anyway to delete from logs
# Remove from logs regardless of Discord deletion success # Remove from logs regardless of Discord deletion success
@@ -488,16 +491,16 @@ class DMLogger:
if deleted_count > 0: if deleted_count > 0:
self._save_user_logs(user_id, logs) self._save_user_logs(user_id, logs)
if discord_deleted: 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: 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 return True
else: 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 return False
except Exception as e: 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 return False
async def delete_all_conversations(self, user_id: int) -> bool: async def delete_all_conversations(self, user_id: int) -> bool:
@@ -507,12 +510,12 @@ class DMLogger:
conversation_count = len(logs["conversations"]) conversation_count = len(logs["conversations"])
if conversation_count == 0: 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 return False
# Find all bot messages to delete from Discord # Find all bot messages to delete from Discord
bot_messages = [conv for conv in logs["conversations"] if conv.get("is_bot_message", False)] 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 # Try to delete all bot messages from Discord
discord_deleted_count = 0 discord_deleted_count = 0
@@ -534,13 +537,13 @@ class DMLogger:
discord_message = await dm_channel.fetch_message(int(message_id)) discord_message = await dm_channel.fetch_message(int(message_id))
await discord_message.delete() await discord_message.delete()
discord_deleted_count += 1 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: 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 # Continue with other messages
except Exception as e: 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 # Continue anyway to delete from logs
# Delete all conversations from logs regardless of Discord deletion success # Delete all conversations from logs regardless of Discord deletion success
@@ -548,14 +551,14 @@ class DMLogger:
self._save_user_logs(user_id, logs) self._save_user_logs(user_id, logs)
if discord_deleted_count > 0: 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: 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 return True
except Exception as e: 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 return False
def delete_user_completely(self, user_id: int) -> bool: def delete_user_completely(self, user_id: int) -> bool:
@@ -564,13 +567,13 @@ class DMLogger:
log_file = self._get_user_log_file(user_id) log_file = self._get_user_log_file(user_id)
if os.path.exists(log_file): if os.path.exists(log_file):
os.remove(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 return True
else: else:
print(f"⚠️ No log file found for user {user_id}") logger.warning(f"No log file found for user {user_id}")
return False return False
except Exception as e: 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 return False
# Global instance # Global instance

View File

@@ -9,6 +9,9 @@ import os
import random import random
import json import json
import globals import globals
from utils.logger import get_logger
logger = get_logger('persona')
# ============================================================================ # ============================================================================
# EVIL MODE PERSISTENCE # 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: with open(EVIL_MODE_STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2) 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: 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(): def load_evil_mode_state():
"""Load evil mode state from JSON file""" """Load evil mode state from JSON file"""
try: try:
if not os.path.exists(EVIL_MODE_STATE_FILE): 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 return False, "evil_neutral", None
with open(EVIL_MODE_STATE_FILE, "r", encoding="utf-8") as f: 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_mode = state.get("evil_mode_enabled", False)
evil_mood = state.get("evil_mood", "evil_neutral") evil_mood = state.get("evil_mood", "evil_neutral")
saved_role_color = state.get("saved_role_color") 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 return evil_mode, evil_mood, saved_role_color
except Exception as e: 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 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() evil_mode, evil_mood, saved_role_color = load_evil_mode_state()
if evil_mode: if evil_mode:
print("😈 Restoring evil mode from previous session...") logger.debug("Restoring evil mode from previous session...")
globals.EVIL_MODE = True globals.EVIL_MODE = True
globals.EVIL_DM_MOOD = evil_mood globals.EVIL_DM_MOOD = evil_mood
globals.EVIL_DM_MOOD_DESCRIPTION = load_evil_mood_description(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: else:
print("🎤 Normal mode active") logger.info("Normal mode active")
return evil_mode 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: with open("evil_miku_lore.txt", "r", encoding="utf-8") as f:
return f.read() return f.read()
except Exception as e: 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]" 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: with open("evil_miku_prompt.txt", "r", encoding="utf-8") as f:
return f.read() return f.read()
except Exception as e: 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]" 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: with open("evil_miku_lyrics.txt", "r", encoding="utf-8") as f:
return f.read() return f.read()
except Exception as e: 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]" 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: with open(path, "r", encoding="utf-8") as f:
return f.read().strip() return f.read().strip()
except FileNotFoundError: 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: try:
with open(os.path.join("moods", "evil", "evil_neutral.txt"), "r", encoding="utf-8") as f: with open(os.path.join("moods", "evil", "evil_neutral.txt"), "r", encoding="utf-8") as f:
return f.read().strip() 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"]: if role.name.lower() in ["miku color", "miku colour", "miku-color"]:
# Convert discord.Color to hex # Convert discord.Color to hex
hex_color = f"#{role.color.value:06x}" 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 return hex_color
print("⚠️ No 'Miku Color' role found in any server") logger.warning("No 'Miku Color' role found in any server")
return None return None
except Exception as e: 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 return None
@@ -377,14 +380,14 @@ async def set_role_color(client, hex_color: str):
if color_role: if color_role:
await color_role.edit(color=discord_color, reason="Evil mode color change") await color_role.edit(color=discord_color, reason="Evil mode color change")
updated_count += 1 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: 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 return updated_count > 0
except Exception as e: except Exception as e:
print(f"⚠️ Failed to set role color: {e}") logger.error(f"Failed to set role color: {e}")
return False 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_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) 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) # Save current role color before changing (if we're actually changing it)
if change_role_color: if change_role_color:
@@ -412,9 +415,9 @@ async def apply_evil_mode_changes(client, change_username=True, change_pfp=True,
if change_username: if change_username:
try: try:
await client.user.edit(username="Evil Miku") 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: 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 # Update nicknames in all servers
if change_nicknames: 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 state to file
save_evil_mode_state() 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): 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_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) 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 globals.EVIL_MODE = False
# Change bot username back # Change bot username back
if change_username: if change_username:
try: try:
await client.user.edit(username="Hatsune Miku") 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: 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 # Update nicknames in all servers back to normal
if change_nicknames: 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() _, _, saved_color = load_evil_mode_state()
if saved_color: if saved_color:
await set_role_color(client, 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: else:
print("⚠️ No saved role color found, skipping color restoration") logger.warning("No saved role color found, skipping color restoration")
except Exception as e: 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 state to file (this will clear saved_role_color since we're back to normal)
save_evil_mode_state(saved_role_color=None) save_evil_mode_state(saved_role_color=None)
print("🎤 Evil Mode disabled!") logger.info("Evil Mode disabled!")
async def update_all_evil_nicknames(client): 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) me = guild.get_member(client.user.id)
if me: if me:
await me.edit(nick=nickname) 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: 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): 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" evil_pfp_path = "memory/profile_pictures/evil_pfp.png"
if not os.path.exists(evil_pfp_path): 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 return False
try: try:
@@ -532,10 +535,10 @@ async def set_evil_profile_picture(client):
avatar_bytes = f.read() avatar_bytes = f.read()
await client.user.edit(avatar=avatar_bytes) await client.user.edit(avatar=avatar_bytes)
print("😈 Set evil profile picture") logger.debug("Set evil profile picture")
return True return True
except Exception as e: 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 return False
@@ -554,12 +557,12 @@ async def restore_normal_profile_picture(client):
avatar_bytes = f.read() avatar_bytes = f.read()
await client.user.edit(avatar=avatar_bytes) 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 return True
except Exception as e: 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 return False
@@ -602,4 +605,4 @@ async def rotate_evil_mood():
globals.EVIL_DM_MOOD_DESCRIPTION = load_evil_mood_description(new_mood) globals.EVIL_DM_MOOD_DESCRIPTION = load_evil_mood_description(new_mood)
save_evil_mode_state() # Save state when mood rotates 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}")

View File

@@ -1,4 +1,4 @@
# face_detector_manager.py Y# face_detector_manager.py
""" """
Manages on-demand starting/stopping of anime-face-detector container Manages on-demand starting/stopping of anime-face-detector container
to free up VRAM when not needed. to free up VRAM when not needed.
@@ -9,6 +9,9 @@ import aiohttp
import subprocess import subprocess
import time import time
from typing import Optional, Dict from typing import Optional, Dict
from utils.logger import get_logger
logger = get_logger('gpu')
class FaceDetectorManager: class FaceDetectorManager:
@@ -31,7 +34,7 @@ class FaceDetectorManager:
""" """
try: try:
if debug: if debug:
print("🚀 Starting anime-face-detector container...") logger.debug("Starting anime-face-detector container...")
# Start container using docker compose # Start container using docker compose
result = subprocess.run( result = subprocess.run(
@@ -44,7 +47,7 @@ class FaceDetectorManager:
if result.returncode != 0: if result.returncode != 0:
if debug: if debug:
print(f"⚠️ Failed to start container: {result.stderr}") logger.error(f"Failed to start container: {result.stderr}")
return False return False
# Wait for API to be ready # Wait for API to be ready
@@ -53,17 +56,17 @@ class FaceDetectorManager:
if await self._check_health(): if await self._check_health():
self.is_running = True self.is_running = True
if debug: if debug:
print(f"Face detector container started and ready") logger.info(f"Face detector container started and ready")
return True return True
await asyncio.sleep(1) await asyncio.sleep(1)
if debug: 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 return False
except Exception as e: except Exception as e:
if debug: if debug:
print(f"⚠️ Error starting face detector container: {e}") logger.error(f"Error starting face detector container: {e}")
return False return False
async def stop_container(self, debug: bool = False) -> bool: async def stop_container(self, debug: bool = False) -> bool:
@@ -75,7 +78,7 @@ class FaceDetectorManager:
""" """
try: try:
if debug: if debug:
print("🛑 Stopping anime-face-detector container...") logger.debug("Stopping anime-face-detector container...")
result = subprocess.run( result = subprocess.run(
["docker", "compose", "stop", self.CONTAINER_NAME], ["docker", "compose", "stop", self.CONTAINER_NAME],
@@ -88,16 +91,16 @@ class FaceDetectorManager:
if result.returncode == 0: if result.returncode == 0:
self.is_running = False self.is_running = False
if debug: if debug:
print("Face detector container stopped") logger.info("Face detector container stopped")
return True return True
else: else:
if debug: if debug:
print(f"⚠️ Failed to stop container: {result.stderr}") logger.error(f"Failed to stop container: {result.stderr}")
return False return False
except Exception as e: except Exception as e:
if debug: if debug:
print(f"⚠️ Error stopping face detector container: {e}") logger.error(f"Error stopping face detector container: {e}")
return False return False
async def _check_health(self) -> bool: async def _check_health(self) -> bool:
@@ -137,7 +140,7 @@ class FaceDetectorManager:
# Step 1: Unload vision model if callback provided # Step 1: Unload vision model if callback provided
if unload_vision_model: if unload_vision_model:
if debug: if debug:
print("📤 Unloading vision model to free VRAM...") logger.debug("Unloading vision model to free VRAM...")
await unload_vision_model() await unload_vision_model()
await asyncio.sleep(2) # Give time for VRAM to clear await asyncio.sleep(2) # Give time for VRAM to clear
@@ -145,7 +148,7 @@ class FaceDetectorManager:
if not self.is_running: if not self.is_running:
if not await self.start_container(debug=debug): if not await self.start_container(debug=debug):
if debug: if debug:
print("⚠️ Could not start face detector container") logger.error("Could not start face detector container")
return None return None
container_was_started = True container_was_started = True
@@ -161,7 +164,7 @@ class FaceDetectorManager:
if reload_vision_model: if reload_vision_model:
if debug: if debug:
print("📥 Reloading vision model...") logger.debug("Reloading vision model...")
await reload_vision_model() await reload_vision_model()
async def _detect_face_api(self, image_bytes: bytes, debug: bool = False) -> Optional[Dict]: async def _detect_face_api(self, image_bytes: bytes, debug: bool = False) -> Optional[Dict]:
@@ -178,14 +181,14 @@ class FaceDetectorManager:
) as response: ) as response:
if response.status != 200: if response.status != 200:
if debug: if debug:
print(f"⚠️ Face detection API returned status {response.status}") logger.warning(f"Face detection API returned status {response.status}")
return None return None
result = await response.json() result = await response.json()
if result.get('count', 0) == 0: if result.get('count', 0) == 0:
if debug: if debug:
print("👤 No faces detected by API") logger.debug("No faces detected by API")
return None return None
detections = result.get('detections', []) detections = result.get('detections', [])
@@ -205,9 +208,9 @@ class FaceDetectorManager:
if debug: if debug:
width = int(x2 - x1) width = int(x2 - x1)
height = int(y2 - y1) height = int(y2 - y1)
print(f"👤 Detected {len(detections)} face(s) via API, using best at ({center_x}, {center_y}) [confidence: {confidence:.2%}]") logger.debug(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}") logger.debug(f" Bounding box: x={int(x1)}, y={int(y1)}, w={width}, h={height}")
print(f" Keypoints: {len(keypoints)} facial landmarks detected") logger.debug(f" Keypoints: {len(keypoints)} facial landmarks detected")
return { return {
'center': (center_x, center_y), 'center': (center_x, center_y),
@@ -219,7 +222,7 @@ class FaceDetectorManager:
except Exception as e: except Exception as e:
if debug: if debug:
print(f"⚠️ Error calling face detection API: {e}") logger.error(f"Error calling face detection API: {e}")
return None return None

View File

@@ -10,7 +10,9 @@ import globals
from utils.twitter_fetcher import fetch_figurine_tweets_latest from utils.twitter_fetcher import fetch_figurine_tweets_latest
from utils.image_handling import analyze_image_with_qwen, download_and_encode_image from utils.image_handling import analyze_image_with_qwen, download_and_encode_image
from utils.llm import query_llama from utils.llm import query_llama
from utils.logger import get_logger
logger = get_logger('bot')
from utils.dm_logger import dm_logger from utils.dm_logger import dm_logger
@@ -37,14 +39,14 @@ def _ensure_dir(path: str) -> None:
def load_subscribers() -> List[int]: def load_subscribers() -> List[int]:
try: try:
if os.path.exists(SUBSCRIBERS_FILE): 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: with open(SUBSCRIBERS_FILE, "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
subs = [int(uid) for uid in data.get("subscribers", [])] 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 return subs
except Exception as e: except Exception as e:
print(f"⚠️ Failed to load figurine subscribers: {e}") logger.error(f"Failed to load figurine subscribers: {e}")
return [] return []
@@ -53,85 +55,85 @@ def save_subscribers(user_ids: List[int]) -> None:
_ensure_dir(SUBSCRIBERS_FILE) _ensure_dir(SUBSCRIBERS_FILE)
# Save as strings to be JS-safe in the API layer if needed # Save as strings to be JS-safe in the API layer if needed
payload = {"subscribers": [str(uid) for uid in user_ids]} 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: with open(SUBSCRIBERS_FILE, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2) json.dump(payload, f, indent=2)
except Exception as e: 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: def add_subscriber(user_id: int) -> bool:
print(f" Figurines: Adding subscriber {user_id}") logger.info(f"Adding subscriber {user_id}")
subscribers = load_subscribers() subscribers = load_subscribers()
if user_id in subscribers: if user_id in subscribers:
print(f" Figurines: Subscriber {user_id} already present") logger.info(f"Subscriber {user_id} already present")
return False return False
subscribers.append(user_id) subscribers.append(user_id)
save_subscribers(subscribers) save_subscribers(subscribers)
print(f"✅ Figurines: Subscriber {user_id} added") logger.info(f"Subscriber {user_id} added")
return True return True
def remove_subscriber(user_id: int) -> bool: def remove_subscriber(user_id: int) -> bool:
print(f"🗑️ Figurines: Removing subscriber {user_id}") logger.info(f"Removing subscriber {user_id}")
subscribers = load_subscribers() subscribers = load_subscribers()
if user_id not in 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 return False
subscribers = [uid for uid in subscribers if uid != user_id] subscribers = [uid for uid in subscribers if uid != user_id]
save_subscribers(subscribers) save_subscribers(subscribers)
print(f"✅ Figurines: Subscriber {user_id} removed") logger.info(f"Subscriber {user_id} removed")
return True return True
def load_sent_tweets() -> List[str]: def load_sent_tweets() -> List[str]:
try: try:
if os.path.exists(SENT_TWEETS_FILE): 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: with open(SENT_TWEETS_FILE, "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
urls = data.get("urls", []) urls = data.get("urls", [])
print(f"📋 Figurines: Loaded {len(urls)} sent tweet URLs") logger.debug(f"Loaded {len(urls)} sent tweet URLs")
return urls return urls
except Exception as e: except Exception as e:
print(f"⚠️ Failed to load figurine sent tweets: {e}") logger.error(f"Failed to load figurine sent tweets: {e}")
return [] return []
def save_sent_tweets(urls: List[str]) -> None: def save_sent_tweets(urls: List[str]) -> None:
try: try:
_ensure_dir(SENT_TWEETS_FILE) _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: with open(SENT_TWEETS_FILE, "w", encoding="utf-8") as f:
json.dump({"urls": urls}, f, indent=2) json.dump({"urls": urls}, f, indent=2)
except Exception as e: 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: async def choose_random_figurine_tweet() -> Dict[str, Any] | None:
"""Fetch figurine tweets from multiple sources, filter out sent, and pick one randomly.""" """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) tweets = await fetch_figurine_tweets_latest(limit_per_source=10)
if not tweets: if not tweets:
print("📭 No figurine tweets found across sources") logger.warning("No figurine tweets found across sources")
return None return None
sent_urls = set(load_sent_tweets()) sent_urls = set(load_sent_tweets())
fresh = [t for t in tweets if t.get("url") not in sent_urls] 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: 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 fresh = tweets
chosen = random.choice(fresh) chosen = random.choice(fresh)
print(f"🎯 Chosen figurine tweet: {chosen.get('url')}") logger.info(f"Chosen figurine tweet: {chosen.get('url')}")
return chosen return chosen
async def send_figurine_dm_to_user(client: discord.Client, user_id: int, tweet: Dict[str, Any]) -> Tuple[bool, str]: 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.""" """Send the figurine tweet to a single subscriber via DM, with analysis and LLM commentary."""
try: 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) user = client.get_user(user_id)
if user is None: if user is None:
# Try fetching # 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) img_desc = await analyze_image_with_qwen(base64_img)
base_prompt += f"\n\nImage looks like: {img_desc}" base_prompt += f"\n\nImage looks like: {img_desc}"
except Exception as e: except Exception as e:
print(f"⚠️ Image analysis failed: {e}") logger.warning(f"Image analysis failed: {e}")
# Include tweet text too # Include tweet text too
tweet_text = tweet.get("text", "").strip() 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) # Send the tweet URL first (convert to fxtwitter for better embeds)
fx_tweet_url = convert_to_fxtwitter(tweet_url) fx_tweet_url = convert_to_fxtwitter(tweet_url)
tweet_message = await dm.send(fx_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 # Log the tweet URL message
dm_logger.log_user_message(user, tweet_message, is_bot_message=True) dm_logger.log_user_message(user, tweet_message, is_bot_message=True)
# Send Miku's comment # Send Miku's comment
comment_message = await dm.send(miku_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 # Log the comment message
dm_logger.log_user_message(user, comment_message, is_bot_message=True) 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 # Use empty user prompt since this was initiated by Miku
globals.conversation_history.setdefault(user_id_str, []).append((tweet_context, miku_comment)) 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" return True, "ok"
except Exception as e: 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}" 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]: 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.""" """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: if tweet_url:
# Use specific 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) tweet = await fetch_specific_tweet_by_url(tweet_url)
if not tweet: if not tweet:
return {"status": "error", "message": "Failed to fetch specified tweet"} return {"status": "error", "message": "Failed to fetch specified tweet"}
else: else:
# Search for a random tweet # 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() tweet = await choose_random_figurine_tweet()
if not tweet: if not tweet:
return {"status": "error", "message": "No figurine tweets found"} 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": [], "failed": [],
"tweet": {"url": tweet.get("url", ""), "username": tweet.get("username", "")} "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 return result
else: else:
result = { 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}], "failed": [{"user_id": str(user_id), "error": msg}],
"message": f"Failed to send DM: {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 return result
async def fetch_specific_tweet_by_url(tweet_url: str) -> Dict[str, Any] | None: async def fetch_specific_tweet_by_url(tweet_url: str) -> Dict[str, Any] | None:
"""Fetch a specific tweet by URL for manual figurine notifications.""" """Fetch a specific tweet by URL for manual figurine notifications."""
try: 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 # Extract tweet ID from URL
tweet_id = None tweet_id = None
if "/status/" in tweet_url: if "/status/" in tweet_url:
try: try:
tweet_id = tweet_url.split("/status/")[1].split("?")[0].split("/")[0] 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: 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 return None
if not tweet_id: if not tweet_id:
print("❌ Figurines: Could not extract tweet ID from URL") logger.error("Could not extract tweet ID from URL")
return None return None
# Set up twscrape API (same pattern as existing functions) # 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 # Try to fetch the tweet using search instead of tweet_details
# Search for the specific tweet ID should return it if accessible # 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 = [] search_results = []
try: try:
# Search using the tweet ID - this should find the specific tweet # Search using the tweet ID - this should find the specific tweet
from twscrape import gather from twscrape import gather
search_results = await gather(api.search(f"{tweet_id}", limit=1)) 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: except Exception as search_error:
print(f"⚠️ Figurines: Search failed: {search_error}") logger.warning(f"Search failed: {search_error}")
return None return None
# Check if we found the tweet # 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: for tweet in search_results:
if str(tweet.id) == str(tweet_id): if str(tweet.id) == str(tweet_id):
tweet_data = tweet tweet_data = tweet
print(f"✅ Figurines: Found matching tweet with ID {tweet.id}") logger.debug(f"Found matching tweet with ID {tweet.id}")
break break
if not tweet_data and search_results: if not tweet_data and search_results:
# If no exact match but we have results, use the first one # If no exact match but we have results, use the first one
tweet_data = search_results[0] 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: if tweet_data:
# Extract data using the same pattern as the working search code # 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" 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 "" text_content = tweet_data.rawContent if hasattr(tweet_data, 'rawContent') else ""
print(f"🔍 Figurines: Found tweet from @{username}") logger.debug(f"Found tweet from @{username}")
print(f"🔍 Figurines: Tweet text: {text_content[:100]}...") 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 # 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 # 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 "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 return result
else: else:
print("❌ Figurines: No tweet found with the specified ID") logger.error("No tweet found with the specified ID")
return None return None
except Exception as e: except Exception as e:
print(f"❌ Figurines: Error fetching tweet by URL: {e}") logger.error(f"Error fetching tweet by URL: {e}")
return None return None
async def send_figurine_dm_to_all_subscribers(client: discord.Client, tweet_url: str = None) -> Dict[str, Any]: 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.""" """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() subscribers = load_subscribers()
if not subscribers: if not subscribers:
print(" Figurines: No subscribers configured") logger.warning("No subscribers configured")
return {"status": "no_subscribers"} return {"status": "no_subscribers"}
if tweet_url: if tweet_url:
# Use specific 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) tweet = await fetch_specific_tweet_by_url(tweet_url)
if not tweet: 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"} return {"status": "no_tweet", "message": "Failed to fetch specified tweet"}
else: else:
# Search for random tweet # Search for random tweet
tweet = await choose_random_figurine_tweet() tweet = await choose_random_figurine_tweet()
if tweet is None: if tweet is None:
print(" Figurines: No tweet to send") logger.warning("No tweet to send")
return {"status": "no_tweet"} return {"status": "no_tweet"}
results = {"sent": [], "failed": []} results = {"sent": [], "failed": []}
@@ -393,7 +395,7 @@ async def send_figurine_dm_to_all_subscribers(client: discord.Client, tweet_url:
if ok: if ok:
results["sent"].append(str(uid)) results["sent"].append(str(uid))
else: 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}) results["failed"].append({"user_id": str(uid), "error": msg})
# Record as sent if at least one success to avoid repeats # 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) save_sent_tweets(sent_urls)
summary = {"status": "ok", **results, "tweet": {"url": tweet.get("url", ""), "username": tweet.get("username", "")}} 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 return summary

View File

@@ -14,6 +14,9 @@ import time
from typing import Optional, Tuple from typing import Optional, Tuple
import globals import globals
from utils.llm import query_llama from utils.llm import query_llama
from utils.logger import get_logger
logger = get_logger('media')
# Image generation detection patterns # Image generation detection patterns
IMAGE_REQUEST_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 recent_threshold = time.time() - 600 # 10 minutes
for file_path in all_files: for file_path in all_files:
if os.path.getmtime(file_path) > recent_threshold: 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 return file_path
except Exception as e: except Exception as e:
print(f"⚠️ Error searching in {output_dir}: {e}") logger.error(f"Error searching in {output_dir}: {e}")
continue continue
return None return None
@@ -156,7 +159,7 @@ async def generate_image_with_comfyui(prompt: str) -> Optional[str]:
# Load the workflow template # Load the workflow template
workflow_path = "Miku_BasicWorkflow.json" workflow_path = "Miku_BasicWorkflow.json"
if not os.path.exists(workflow_path): 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 return None
with open(workflow_path, 'r') as f: 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: async with test_session.get(f"{url}/system_stats", timeout=timeout) as test_response:
if test_response.status == 200: if test_response.status == 200:
comfyui_url = url comfyui_url = url
print(f"ComfyUI found at: {url}") logger.debug(f"ComfyUI found at: {url}")
break break
except: except:
continue continue
if not comfyui_url: 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 return None
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
# Submit the generation request # Submit the generation request
async with session.post(f"{comfyui_url}/prompt", json=payload) as response: async with session.post(f"{comfyui_url}/prompt", json=payload) as response:
if response.status != 200: if response.status != 200:
print(f"ComfyUI request failed: {response.status}") logger.error(f"ComfyUI request failed: {response.status}")
return None return None
result = await response.json() result = await response.json()
prompt_id = result.get("prompt_id") prompt_id = result.get("prompt_id")
if not prompt_id: if not prompt_id:
print("No prompt_id received from ComfyUI") logger.error("No prompt_id received from ComfyUI")
return None 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) # Poll for completion (timeout after 5 minutes)
timeout = 300 # 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 # Verify the file exists before returning
if os.path.exists(image_path): if os.path.exists(image_path):
print(f"Image generated successfully: {image_path}") logger.info(f"Image generated successfully: {image_path}")
return image_path return image_path
else: else:
# Try alternative paths in case of different mounting # Try alternative paths in case of different mounting
alt_path = os.path.join("/app/ComfyUI/output", filename) alt_path = os.path.join("/app/ComfyUI/output", filename)
if os.path.exists(alt_path): if os.path.exists(alt_path):
print(f"Image generated successfully: {alt_path}") logger.info(f"Image generated successfully: {alt_path}")
return alt_path return alt_path
else: 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 continue
# If we couldn't find the image via API, try the fallback method # 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) fallback_image = find_latest_generated_image(prompt_id)
if fallback_image: if fallback_image:
return fallback_image return fallback_image
@@ -263,19 +266,19 @@ async def generate_image_with_comfyui(prompt: str) -> Optional[str]:
# Wait before polling again # Wait before polling again
await asyncio.sleep(2) await asyncio.sleep(2)
print("ComfyUI generation timed out") logger.error("ComfyUI generation timed out")
# Final fallback: look for the most recent image # 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) fallback_image = find_latest_generated_image(prompt_id)
if fallback_image: 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 fallback_image
return None return None
except Exception as e: 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 return None
async def handle_image_generation_request(message, prompt: str) -> bool: 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 # Start typing to show we're working
async with message.channel.typing(): async with message.channel.typing():
# Generate the image # 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) image_path = await generate_image_with_comfyui(prompt)
if image_path and os.path.exists(image_path): 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) 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 # Log to DM history if it's a DM
if is_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) 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) 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 return False
except Exception as e: 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 # Send error message
try: try:

View File

@@ -10,6 +10,10 @@ from PIL import Image
import re import re
import globals import globals
from utils.logger import get_logger
logger = get_logger('vision')
# No need for switch_model anymore - llama-swap handles this automatically # 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) match = re.search(r'tenor\.com/(\d+)\.gif', tenor_url)
if not match: 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 return None
gif_id = match.group(1) 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 aiohttp.ClientSession() as session:
async with session.head(media_url) as resp: async with session.head(media_url) as resp:
if resp.status == 200: if resp.status == 200:
print(f"Found Tenor GIF: {media_url}") logger.debug(f"Found Tenor GIF: {media_url}")
return media_url return media_url
# If that didn't work, try alternative formats # 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 aiohttp.ClientSession() as session:
async with session.head(alt_url) as resp: async with session.head(alt_url) as resp:
if resp.status == 200: if resp.status == 200:
print(f"Found Tenor GIF (alternative): {alt_url}") logger.debug(f"Found Tenor GIF (alternative): {alt_url}")
return 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 return None
except Exception as e: except Exception as e:
print(f"⚠️ Error extracting Tenor GIF URL: {e}") logger.error(f"Error extracting Tenor GIF URL: {e}")
return None return None
@@ -114,7 +118,7 @@ async def convert_gif_to_mp4(gif_bytes):
with open(temp_mp4_path, 'rb') as f: with open(temp_mp4_path, 'rb') as f:
mp4_bytes = f.read() 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 return mp4_bytes
finally: finally:
@@ -125,10 +129,10 @@ async def convert_gif_to_mp4(gif_bytes):
os.remove(temp_mp4_path) os.remove(temp_mp4_path)
except subprocess.CalledProcessError as e: 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 return None
except Exception as e: except Exception as e:
print(f"⚠️ Error converting GIF to MP4: {e}") logger.error(f"Error converting GIF to MP4: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return None return None
@@ -165,7 +169,7 @@ async def extract_video_frames(video_bytes, num_frames=4):
if frames: if frames:
return frames return frames
except Exception as e: 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 # For video files (MP4, WebM, etc.), use ffmpeg
import subprocess import subprocess
@@ -222,7 +226,7 @@ async def extract_video_frames(video_bytes, num_frames=4):
os.remove(temp_video_path) os.remove(temp_video_path)
except Exception as e: except Exception as e:
print(f"⚠️ Error extracting frames: {e}") logger.error(f"Error extracting frames: {e}")
import traceback import traceback
traceback.print_exc() 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.") return data.get("choices", [{}])[0].get("message", {}).get("content", "No description.")
else: else:
error_text = await response.text() 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}" return f"Error analyzing image: {response.status}"
except Exception as e: 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)}" 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.") return data.get("choices", [{}])[0].get("message", {}).get("content", "No description.")
else: else:
error_text = await response.text() 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}" return f"Error analyzing video: {response.status}"
except Exception as e: 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)}" return f"Error analyzing video: {str(e)}"

View File

@@ -3,6 +3,9 @@
import random import random
import globals import globals
from utils.llm import query_llama # Adjust path as needed 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): 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) await message.add_reaction(emoji)
globals.kindness_reacted_messages.add(message.id) globals.kindness_reacted_messages.add(message.id)
message.kindness_reacted = True # Mark as done 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: except Exception as e:
print(f"⚠️ Error adding reaction: {e}") logger.error(f"Error adding reaction: {e}")
return return
# 2. If not after_reply, defer model-based check # 2. If not after_reply, defer model-based check
if not after_reply: if not after_reply:
print("🗝️ No kindness via keywords. Deferring...") logger.debug("No kindness via keywords. Deferring...")
return return
# 3. Model-based detection # 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"): if result.strip().lower().startswith("yes"):
await message.add_reaction(emoji) await message.add_reaction(emoji)
globals.kindness_reacted_messages.add(message.id) globals.kindness_reacted_messages.add(message.id)
print("Kindness detected via model. Reacted.") logger.info("Kindness detected via model. Reacted.")
else: else:
print("🧊 No kindness detected.") logger.debug("No kindness detected.")
except Exception as e: except Exception as e:
print(f"⚠️ Error during kindness analysis: {e}") logger.error(f"Error during kindness analysis: {e}")

View File

@@ -10,6 +10,10 @@ import os
from utils.context_manager import get_context_for_response_type, get_complete_context from utils.context_manager import get_context_for_response_type, get_complete_context
from utils.moods import load_mood_description from utils.moods import load_mood_description
from utils.conversation_history import conversation_history from utils.conversation_history import conversation_history
from utils.logger import get_logger
logger = get_logger('llm')
def get_current_gpu_url(): def get_current_gpu_url():
"""Get the URL for the currently selected GPU for text models""" """Get the URL for the currently selected GPU for text models"""
@@ -23,7 +27,7 @@ def get_current_gpu_url():
else: else:
return globals.LLAMA_URL return globals.LLAMA_URL
except Exception as e: 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 # Default to NVIDIA if state file doesn't exist
return globals.LLAMA_URL 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 model is None:
if evil_mode: if evil_mode:
model = globals.EVIL_TEXT_MODEL # Use DarkIdol uncensored model model = globals.EVIL_TEXT_MODEL # Use DarkIdol uncensored model
print(f"😈 Using evil model: {model}") logger.info(f"Using evil model: {model}")
else: else:
model = globals.TEXT_MODEL model = globals.TEXT_MODEL
@@ -155,7 +159,7 @@ You ARE Miku. Act like it."""
is_sleeping = False is_sleeping = False
forced_angry_until = None forced_angry_until = None
just_woken_up = False 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: else:
current_mood = globals.DM_MOOD_DESCRIPTION # Default to DM mood current_mood = globals.DM_MOOD_DESCRIPTION # Default to DM mood
current_mood_name = globals.DM_MOOD # Default to DM mood name 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 is_sleeping = server_config.is_sleeping
forced_angry_until = server_config.forced_angry_until forced_angry_until = server_config.forced_angry_until
just_woken_up = server_config.just_woken_up 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: 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: 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 # Fall back to DM mood if server mood fails
elif not evil_mode: 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) # 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: 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: try:
# Get current GPU URL based on user selection # Get current GPU URL based on user selection
llama_url = get_current_gpu_url() 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 # Add timeout to prevent hanging indefinitely
timeout = aiohttp.ClientTimeout(total=300) # 300 second timeout 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 return reply
else: else:
error_text = await response.text() 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 # Don't save error responses to conversation history
return f"Error: {response.status}" return f"Error: {response.status}"
except asyncio.TimeoutError: except asyncio.TimeoutError:
return "Sorry, the response took too long. Please try again." return "Sorry, the response took too long. Please try again."
except Exception as e: 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)}" return f"Sorry, there was an error: {str(e)}"
# Backward compatibility alias for existing code # Backward compatibility alias for existing code

286
bot/utils/log_config.py Normal file
View File

@@ -0,0 +1,286 @@
"""
Log Configuration Manager
Handles runtime configuration updates for the logging system.
Provides API for the web UI to update log settings without restarting the bot.
"""
from pathlib import Path
from typing import Dict, List, Optional
import json
try:
from utils.logger import get_logger
logger = get_logger('core')
except Exception:
logger = None
CONFIG_FILE = Path('/app/memory/log_settings.json')
def load_config() -> Dict:
"""Load log configuration from file."""
from utils.logger import get_log_config
return get_log_config()
def save_config(config: Dict) -> bool:
"""
Save log configuration to file.
Args:
config: Configuration dictionary
Returns:
True if successful, False otherwise
"""
try:
from utils.logger import save_config
save_config(config)
return True
except Exception as e:
if logger:
logger.error(f"Failed to save log config: {e}")
print(f"Failed to save log config: {e}")
return False
def update_component(component: str, enabled: bool = None, enabled_levels: List[str] = None) -> bool:
"""
Update a single component's configuration.
Args:
component: Component name
enabled: Enable/disable the component
enabled_levels: List of log levels to enable (DEBUG, INFO, WARNING, ERROR, CRITICAL, API)
Returns:
True if successful, False otherwise
"""
try:
config = load_config()
if component not in config['components']:
return False
if enabled is not None:
config['components'][component]['enabled'] = enabled
if enabled_levels is not None:
valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'API']
# Validate all levels
for level in enabled_levels:
if level.upper() not in valid_levels:
return False
config['components'][component]['enabled_levels'] = [l.upper() for l in enabled_levels]
return save_config(config)
except Exception as e:
if logger:
logger.error(f"Failed to update component {component}: {e}")
print(f"Failed to update component {component}: {e}")
return False
def update_global_level(level: str, enabled: bool) -> bool:
"""
Enable or disable a specific log level across all components.
Args:
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL, API)
enabled: True to enable, False to disable
Returns:
True if successful, False otherwise
"""
try:
level = level.upper()
valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'API']
if level not in valid_levels:
return False
config = load_config()
# Update all components
for component_name in config['components'].keys():
current_levels = config['components'][component_name].get('enabled_levels', [])
if enabled:
# Add level if not present
if level not in current_levels:
current_levels.append(level)
else:
# Remove level if present
if level in current_levels:
current_levels.remove(level)
config['components'][component_name]['enabled_levels'] = current_levels
return save_config(config)
except Exception as e:
if logger:
logger.error(f"Failed to update global level {level}: {e}")
print(f"Failed to update global level {level}: {e}")
return False
def update_timestamp_format(format_type: str) -> bool:
"""
Update timestamp format for all log outputs.
Args:
format_type: Format type - 'off', 'time', 'date', or 'datetime'
Returns:
True if successful, False otherwise
"""
try:
valid_formats = ['off', 'time', 'date', 'datetime']
if format_type not in valid_formats:
return False
config = load_config()
if 'formatting' not in config:
config['formatting'] = {}
config['formatting']['timestamp_format'] = format_type
return save_config(config)
except Exception as e:
if logger:
logger.error(f"Failed to update timestamp format: {e}")
print(f"Failed to update timestamp format: {e}")
return False
def update_api_filters(
exclude_paths: List[str] = None,
exclude_status: List[int] = None,
include_slow_requests: bool = None,
slow_threshold_ms: int = None
) -> bool:
"""
Update API request filtering configuration.
Args:
exclude_paths: List of path patterns to exclude (e.g., ['/health', '/static/*'])
exclude_status: List of HTTP status codes to exclude (e.g., [200, 304])
include_slow_requests: Whether to log slow requests
slow_threshold_ms: Threshold for slow requests in milliseconds
Returns:
True if successful, False otherwise
"""
try:
config = load_config()
if 'api.requests' not in config['components']:
return False
filters = config['components']['api.requests'].get('filters', {})
if exclude_paths is not None:
filters['exclude_paths'] = exclude_paths
if exclude_status is not None:
filters['exclude_status'] = exclude_status
if include_slow_requests is not None:
filters['include_slow_requests'] = include_slow_requests
if slow_threshold_ms is not None:
filters['slow_threshold_ms'] = slow_threshold_ms
config['components']['api.requests']['filters'] = filters
return save_config(config)
except Exception as e:
if logger:
logger.error(f"Failed to update API filters: {e}")
print(f"Failed to update API filters: {e}")
return False
def reset_to_defaults() -> bool:
"""
Reset configuration to defaults.
Returns:
True if successful, False otherwise
"""
try:
from utils.logger import get_default_config, save_config
default_config = get_default_config()
save_config(default_config)
return True
except Exception as e:
if logger:
logger.error(f"Failed to reset config: {e}")
print(f"Failed to reset config: {e}")
return False
def get_component_config(component: str) -> Optional[Dict]:
"""
Get configuration for a specific component.
Args:
component: Component name
Returns:
Component configuration dictionary or None
"""
try:
config = load_config()
return config['components'].get(component)
except Exception:
return None
def is_component_enabled(component: str) -> bool:
"""
Check if a component is enabled.
Args:
component: Component name
Returns:
True if enabled, False otherwise
"""
component_config = get_component_config(component)
if component_config is None:
return True # Default to enabled
return component_config.get('enabled', True)
def get_component_level(component: str) -> str:
"""
Get log level for a component.
Args:
component: Component name
Returns:
Log level string (e.g., 'INFO', 'DEBUG')
"""
component_config = get_component_config(component)
if component_config is None:
return 'INFO' # Default level
return component_config.get('level', 'INFO')
def reload_all_loggers():
"""Reload all logger configurations."""
try:
from utils.logger import reload_config
reload_config()
return True
except Exception as e:
if logger:
logger.error(f"Failed to reload loggers: {e}")
print(f"Failed to reload loggers: {e}")
return False

395
bot/utils/logger.py Normal file
View File

@@ -0,0 +1,395 @@
"""
Centralized Logging System for Miku Discord Bot
This module provides a robust, component-based logging system with:
- Configurable log levels per component
- Emoji-based log formatting
- Multiple output handlers (console, separate log files per component)
- Runtime configuration updates
- API request filtering
- Docker-compatible output
Usage:
from utils.logger import get_logger
logger = get_logger('bot')
logger.info("Bot started successfully")
logger.error("Failed to connect", exc_info=True)
"""
import logging
import sys
import os
from pathlib import Path
from typing import Optional, Dict
from logging.handlers import RotatingFileHandler
import json
# Log level emojis
LEVEL_EMOJIS = {
'DEBUG': '🔍',
'INFO': '',
'WARNING': '⚠️',
'ERROR': '',
'CRITICAL': '🔥',
'API': '🌐',
}
# Custom API log level (between INFO and WARNING)
API_LEVEL = 25
logging.addLevelName(API_LEVEL, 'API')
# Component definitions
COMPONENTS = {
'bot': 'Main bot lifecycle and events',
'api': 'FastAPI endpoints (non-HTTP)',
'api.requests': 'HTTP request/response logs',
'autonomous': 'Autonomous messaging system',
'persona': 'Bipolar/persona dialogue system',
'vision': 'Image and video processing',
'llm': 'LLM API calls and interactions',
'conversation': 'Conversation history management',
'mood': 'Mood system and state changes',
'dm': 'Direct message handling',
'scheduled': 'Scheduled tasks and cron jobs',
'gpu': 'GPU routing and model management',
'media': 'Media processing (audio, video, images)',
'server': 'Server management and configuration',
'commands': 'Command handling and routing',
'sentiment': 'Sentiment analysis',
'core': 'Core utilities and helpers',
'apscheduler': 'Job scheduler logs (APScheduler)',
}
# Global configuration
_log_config: Optional[Dict] = None
_loggers: Dict[str, logging.Logger] = {}
_handlers_initialized = False
# Log directory (in mounted volume so logs persist)
LOG_DIR = Path(os.getenv('LOG_DIR', '/app/memory/logs'))
class EmojiFormatter(logging.Formatter):
"""Custom formatter that adds emojis and colors to log messages."""
def __init__(self, use_emojis=True, use_colors=False, timestamp_format='datetime', *args, **kwargs):
super().__init__(*args, **kwargs)
self.use_emojis = use_emojis
self.use_colors = use_colors
self.timestamp_format = timestamp_format
def format(self, record):
# Add emoji prefix
if self.use_emojis:
emoji = LEVEL_EMOJIS.get(record.levelname, '')
record.levelname_emoji = f"{emoji} {record.levelname}"
else:
record.levelname_emoji = record.levelname
# Format timestamp based on settings
if self.timestamp_format == 'off':
record.timestamp_formatted = ''
elif self.timestamp_format == 'time':
record.timestamp_formatted = self.formatTime(record, '%H:%M:%S') + ' '
elif self.timestamp_format == 'date':
record.timestamp_formatted = self.formatTime(record, '%Y-%m-%d') + ' '
elif self.timestamp_format == 'datetime':
record.timestamp_formatted = self.formatTime(record, '%Y-%m-%d %H:%M:%S') + ' '
else:
# Default to datetime if invalid option
record.timestamp_formatted = self.formatTime(record, '%Y-%m-%d %H:%M:%S') + ' '
# Format the message
return super().format(record)
class ComponentFilter(logging.Filter):
"""Filter logs based on component configuration with individual level toggles."""
def __init__(self, component_name: str):
super().__init__()
self.component_name = component_name
def filter(self, record):
"""Check if this log should be output based on enabled levels."""
config = get_log_config()
if not config:
return True
component_config = config.get('components', {}).get(self.component_name, {})
# Check if component is enabled
if not component_config.get('enabled', True):
return False
# Check if specific log level is enabled
enabled_levels = component_config.get('enabled_levels', ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'API'])
# Get the level name for this record
level_name = logging.getLevelName(record.levelno)
return level_name in enabled_levels
def get_log_config() -> Optional[Dict]:
"""Get current log configuration."""
global _log_config
if _log_config is None:
# Try to load from file
config_path = Path('/app/memory/log_settings.json')
if config_path.exists():
try:
with open(config_path, 'r') as f:
_log_config = json.load(f)
except Exception:
_log_config = get_default_config()
else:
_log_config = get_default_config()
return _log_config
def get_default_config() -> Dict:
"""Get default logging configuration."""
# Read from environment variables
# Enable api.requests by default (now that uvicorn access logs are disabled)
enable_api_requests = os.getenv('LOG_ENABLE_API_REQUESTS', 'true').lower() == 'true'
use_emojis = os.getenv('LOG_USE_EMOJIS', 'true').lower() == 'true'
config = {
'version': '1.0',
'formatting': {
'use_emojis': use_emojis,
'use_colors': False,
'timestamp_format': 'datetime' # Options: 'off', 'time', 'date', 'datetime'
},
'components': {}
}
# Set defaults for each component
for component in COMPONENTS.keys():
if component == 'api.requests':
# API requests component defaults to only ERROR and CRITICAL
default_levels = ['ERROR', 'CRITICAL'] if not enable_api_requests else ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'API']
config['components'][component] = {
'enabled': enable_api_requests,
'enabled_levels': default_levels,
'filters': {
'exclude_paths': ['/health', '/static/*'],
'exclude_status': [200, 304] if not enable_api_requests else [],
'include_slow_requests': True,
'slow_threshold_ms': 1000
}
}
elif component == 'apscheduler':
# APScheduler defaults to WARNING and above (lots of INFO noise)
config['components'][component] = {
'enabled': True,
'enabled_levels': ['WARNING', 'ERROR', 'CRITICAL']
}
else:
# All other components default to all levels enabled
config['components'][component] = {
'enabled': True,
'enabled_levels': ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
}
return config
def reload_config():
"""Reload configuration from file."""
global _log_config
_log_config = None
get_log_config()
# Update all existing loggers
for component_name, logger in _loggers.items():
_configure_logger(logger, component_name)
def save_config(config: Dict):
"""Save configuration to file."""
global _log_config
_log_config = config
config_path = Path('/app/memory/log_settings.json')
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, 'w') as f:
json.dump(config, f, indent=2)
# Reload all loggers
reload_config()
def _setup_handlers():
"""Set up log handlers (console and file)."""
global _handlers_initialized
if _handlers_initialized:
return
# Create log directory
LOG_DIR.mkdir(parents=True, exist_ok=True)
_handlers_initialized = True
def _configure_logger(logger: logging.Logger, component_name: str):
"""Configure a logger with handlers and filters."""
config = get_log_config()
formatting = config.get('formatting', {})
# Clear existing handlers
logger.handlers.clear()
# Set logger level to DEBUG so handlers can filter
logger.setLevel(logging.DEBUG)
logger.propagate = False
# Create formatter
timestamp_format = formatting.get('timestamp_format', 'datetime') # 'off', 'time', 'date', or 'datetime'
use_emojis = formatting.get('use_emojis', True)
use_colors = formatting.get('use_colors', False)
# Console handler - goes to Docker logs
console_handler = logging.StreamHandler(sys.stdout)
console_formatter = EmojiFormatter(
fmt='%(timestamp_formatted)s[%(levelname_emoji)s] [%(name)s] %(message)s',
use_emojis=use_emojis,
use_colors=use_colors,
timestamp_format=timestamp_format
)
console_handler.setFormatter(console_formatter)
console_handler.addFilter(ComponentFilter(component_name))
logger.addHandler(console_handler)
# File handler - separate file per component
log_file = LOG_DIR / f'{component_name.replace(".", "_")}.log'
file_handler = RotatingFileHandler(
log_file,
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5,
encoding='utf-8'
)
file_formatter = EmojiFormatter(
fmt='%(timestamp_formatted)s[%(levelname)s] [%(name)s] %(message)s',
use_emojis=False, # No emojis in file logs
use_colors=False,
timestamp_format=timestamp_format
)
file_handler.setFormatter(file_formatter)
file_handler.addFilter(ComponentFilter(component_name))
logger.addHandler(file_handler)
def get_logger(component: str) -> logging.Logger:
"""
Get a logger for a specific component.
Args:
component: Component name (e.g., 'bot', 'api', 'autonomous')
Returns:
Configured logger instance
Example:
logger = get_logger('bot')
logger.info("Bot started")
logger.error("Connection failed", exc_info=True)
"""
if component not in COMPONENTS:
raise ValueError(
f"Unknown component '{component}'. "
f"Available: {', '.join(COMPONENTS.keys())}"
)
if component in _loggers:
return _loggers[component]
# Setup handlers if not done
_setup_handlers()
# Create logger
logger = logging.Logger(component)
# Add custom API level method
def api(self, message, *args, **kwargs):
if self.isEnabledFor(API_LEVEL):
self._log(API_LEVEL, message, args, **kwargs)
logger.api = lambda msg, *args, **kwargs: api(logger, msg, *args, **kwargs)
# Configure logger
_configure_logger(logger, component)
# Cache it
_loggers[component] = logger
return logger
def list_components() -> Dict[str, str]:
"""Get list of all available components with descriptions."""
return COMPONENTS.copy()
def get_component_stats() -> Dict[str, Dict]:
"""Get statistics about each component's logging."""
stats = {}
for component in COMPONENTS.keys():
log_file = LOG_DIR / f'{component.replace(".", "_")}.log'
stats[component] = {
'enabled': True, # Will be updated from config
'log_file': str(log_file),
'file_exists': log_file.exists(),
'file_size': log_file.stat().st_size if log_file.exists() else 0,
}
# Update from config
config = get_log_config()
component_config = config.get('components', {}).get(component, {})
stats[component]['enabled'] = component_config.get('enabled', True)
stats[component]['level'] = component_config.get('level', 'INFO')
stats[component]['enabled_levels'] = component_config.get('enabled_levels', ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])
return stats
def intercept_external_loggers():
"""
Intercept logs from external libraries (APScheduler, etc.) and route them through our system.
Call this after initializing your application.
"""
# Intercept APScheduler loggers
apscheduler_loggers = [
'apscheduler',
'apscheduler.scheduler',
'apscheduler.executors',
'apscheduler.jobstores',
]
our_logger = get_logger('apscheduler')
for logger_name in apscheduler_loggers:
ext_logger = logging.getLogger(logger_name)
# Remove existing handlers
ext_logger.handlers.clear()
ext_logger.propagate = False
# Add our handlers
for handler in our_logger.handlers:
ext_logger.addHandler(handler)
# Set level
ext_logger.setLevel(logging.DEBUG)
# Initialize on import
_setup_handlers()

View File

@@ -1,6 +1,9 @@
# utils/media.py # utils/media.py
import subprocess import subprocess
from utils.logger import get_logger
logger = get_logger('media')
async def overlay_username_with_ffmpeg(base_video_path, output_path, username): async def overlay_username_with_ffmpeg(base_video_path, output_path, username):
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" 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: try:
subprocess.run(ffmpeg_command, check=True) 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: except subprocess.CalledProcessError as e:
print(f"⚠️ FFmpeg error: {e}") logger.error(f"FFmpeg error: {e}")

View File

@@ -7,6 +7,9 @@ import asyncio
from discord.ext import tasks from discord.ext import tasks
import globals import globals
import datetime import datetime
from utils.logger import get_logger
logger = get_logger('mood')
MOOD_EMOJIS = { MOOD_EMOJIS = {
"asleep": "💤", "asleep": "💤",
@@ -47,7 +50,7 @@ def load_mood_description(mood_name: str) -> str:
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f:
return f.read().strip() return f.read().strip()
except FileNotFoundError: 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 a default mood description instead of recursive call
return "I'm feeling neutral and balanced today." 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 # For server context, check against server's current mood
current_mood = server_context.get('current_mood_name', 'neutral') current_mood = server_context.get('current_mood_name', 'neutral')
if current_mood != "sleepy": 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 continue
else: else:
# For DM context, check against DM mood # For DM context, check against DM mood
if globals.DM_MOOD != "sleepy": 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 continue
for phrase in phrases: for phrase in phrases:
if phrase.lower() in response_text.lower(): if phrase.lower() in response_text.lower():
print(f"*️⃣ Mood keyword triggered: {phrase}") logger.info(f"Mood keyword triggered: {phrase}")
return mood return mood
return None return None
@@ -155,13 +158,13 @@ async def rotate_dm_mood():
globals.DM_MOOD = new_mood globals.DM_MOOD = new_mood
globals.DM_MOOD_DESCRIPTION = load_mood_description(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. # Note: We don't update server nicknames here because servers have their own independent moods.
# DM mood only affects direct messages to users. # DM mood only affects direct messages to users.
except Exception as e: 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(): 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, This function incorrectly used DM mood to update all server nicknames,
breaking the independent per-server mood system. breaking the independent per-server mood system.
""" """
print("⚠️ WARNING: update_all_server_nicknames() is deprecated and should not be called!") logger.warning("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("Use update_server_nickname(guild_id) for per-server nickname updates instead.")
# Do nothing - this function should not modify nicknames # Do nothing - this function should not modify nicknames
async def nickname_mood_emoji(guild_id: int): 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): async def update_server_nickname(guild_id: int):
"""Update nickname for a specific server based on its mood""" """Update nickname for a specific server based on its mood"""
try: 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 # Check if bot is ready
if not globals.client.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 return
# Check if evil mode is active # Check if evil mode is active
@@ -196,7 +199,7 @@ async def update_server_nickname(guild_id: int):
from server_manager import server_manager from server_manager import server_manager
server_config = server_manager.get_server_config(guild_id) server_config = server_manager.get_server_config(guild_id)
if not server_config: 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 return
if evil_mode: if evil_mode:
@@ -209,29 +212,29 @@ async def update_server_nickname(guild_id: int):
emoji = MOOD_EMOJIS.get(mood, "") emoji = MOOD_EMOJIS.get(mood, "")
base_name = "Hatsune Miku" base_name = "Hatsune Miku"
print(f"🔍 Server {guild_id} mood is: {mood} (evil_mode={evil_mode})") logger.debug(f"Server {guild_id} mood is: {mood} (evil_mode={evil_mode})")
print(f"🔍 Using emoji: {emoji}") logger.debug(f"Using emoji: {emoji}")
nickname = f"{base_name}{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) guild = globals.client.get_guild(guild_id)
if guild: if guild:
print(f"🔍 Found guild: {guild.name}") logger.debug(f"Found guild: {guild.name}")
me = guild.get_member(globals.BOT_USER.id) me = guild.get_member(globals.BOT_USER.id)
if me is not None: if me is not None:
print(f"🔍 Found bot member: {me.display_name}") logger.debug(f"Found bot member: {me.display_name}")
try: try:
await me.edit(nick=nickname) 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: 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: 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: else:
print(f"⚠️ Could not find guild {guild_id}") logger.warning(f"Could not find guild {guild_id}")
except Exception as e: 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 import traceback
traceback.print_exc() traceback.print_exc()
@@ -268,7 +271,7 @@ async def rotate_server_mood(guild_id: int):
# Block transition to asleep unless coming from sleepy # Block transition to asleep unless coming from sleepy
if new_mood_name == "asleep" and old_mood_name != "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 # Try to get a different mood
attempts = 0 attempts = 0
while (new_mood_name == "asleep" or new_mood_name == old_mood_name) and attempts < 5: 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 from utils.autonomous import on_mood_change
on_mood_change(guild_id, new_mood_name) on_mood_change(guild_id, new_mood_name)
except Exception as mood_notify_error: 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 transitioning to asleep, set up auto-wake
if new_mood_name == "asleep": if new_mood_name == "asleep":
@@ -298,22 +301,22 @@ async def rotate_server_mood(guild_id: int):
from utils.autonomous import on_mood_change from utils.autonomous import on_mood_change
on_mood_change(guild_id, "neutral") on_mood_change(guild_id, "neutral")
except Exception as mood_notify_error: 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) 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()) 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 # Update nickname for this specific server
await update_server_nickname(guild_id) 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: 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(): async def clear_angry_mood_after_delay():
"""Clear angry mood after delay (legacy function - now handled per-server)""" """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 pass

View File

@@ -15,6 +15,15 @@ This system is designed to be lightweight on LLM calls:
- Only escalates to argument system when tension threshold is reached - 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 os
import json import json
import time import time
@@ -38,7 +47,7 @@ ARGUMENT_TENSION_THRESHOLD = 0.75 # Tension level that triggers argument escal
# Initial trigger settings # Initial trigger settings
INTERJECTION_COOLDOWN_HARD = 180 # 3 minutes hard block INTERJECTION_COOLDOWN_HARD = 180 # 3 minutes hard block
INTERJECTION_COOLDOWN_SOFT = 900 # 15 minutes for full recovery 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) # INTERJECTION SCORER (Initial Trigger Decision)
@@ -62,15 +71,15 @@ class InterjectionScorer:
def sentiment_analyzer(self): def sentiment_analyzer(self):
"""Lazy load sentiment analyzer""" """Lazy load sentiment analyzer"""
if self._sentiment_analyzer is None: if self._sentiment_analyzer is None:
print("🔄 Loading sentiment analyzer for persona dialogue...") logger.debug("Loading sentiment analyzer for persona dialogue...")
try: try:
self._sentiment_analyzer = pipeline( self._sentiment_analyzer = pipeline(
"sentiment-analysis", "sentiment-analysis",
model="distilbert-base-uncased-finetuned-sst-2-english" model="distilbert-base-uncased-finetuned-sst-2-english"
) )
print("Sentiment analyzer loaded") logger.info("Sentiment analyzer loaded")
except Exception as e: 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 self._sentiment_analyzer = None
return self._sentiment_analyzer return self._sentiment_analyzer
@@ -97,8 +106,8 @@ class InterjectionScorer:
opposite_persona = "evil" if current_persona == "miku" else "miku" opposite_persona = "evil" if current_persona == "miku" else "miku"
print(f"🔍 [Interjection] Analyzing content: '{message.content[:100]}...'") logger.debug(f"[Interjection] Analyzing content: '{message.content[:100]}...'")
print(f"🔍 [Interjection] Current persona: {current_persona}, Opposite: {opposite_persona}") logger.debug(f"[Interjection] Current persona: {current_persona}, Opposite: {opposite_persona}")
# Calculate score from various factors # Calculate score from various factors
score = 0.0 score = 0.0
@@ -106,7 +115,7 @@ class InterjectionScorer:
# Factor 1: Direct addressing (automatic trigger) # Factor 1: Direct addressing (automatic trigger)
if self._mentions_opposite(message.content, opposite_persona): 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 return True, "directly_addressed", 1.0
# Factor 2: Topic relevance # Factor 2: Topic relevance
@@ -147,8 +156,8 @@ class InterjectionScorer:
reason_str = " | ".join(reasons) if reasons else "no_triggers" reason_str = " | ".join(reasons) if reasons else "no_triggers"
if should_interject: if should_interject:
print(f"{opposite_persona.upper()} WILL INTERJECT (score: {score:.2f})") logger.info(f"{opposite_persona.upper()} WILL INTERJECT (score: {score:.2f})")
print(f" Reasons: {reason_str}") logger.info(f" Reasons: {reason_str}")
return should_interject, reason_str, score return should_interject, reason_str, score
@@ -156,12 +165,12 @@ class InterjectionScorer:
"""Fast rejection criteria""" """Fast rejection criteria"""
# System messages # System messages
if message.type != discord.MessageType.default: 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 return False
# Bipolar mode must be enabled # Bipolar mode must be enabled
if not globals.BIPOLAR_MODE: if not globals.BIPOLAR_MODE:
print(f"[Basic Filter] Bipolar mode not enabled") logger.debug(f"[Basic Filter] Bipolar mode not enabled")
return False return False
# Allow bot's own messages (we're checking them for interjections!) # 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: if message.author.bot and not message.webhook_id:
# Check if it's our own bot # Check if it's our own bot
if message.author.id != globals.client.user.id: 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 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 return True
def _mentions_opposite(self, content: str, opposite_persona: str) -> bool: 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) return min(confidence * 0.6 + intensity_markers, 1.0)
except Exception as e: except Exception as e:
print(f"⚠️ Sentiment analysis error: {e}") logger.error(f"Sentiment analysis error: {e}")
return 0.5 return 0.5
def _detect_personality_clash(self, content: str, opposite_persona: str) -> float: def _detect_personality_clash(self, content: str, opposite_persona: str) -> float:
@@ -364,15 +373,15 @@ class PersonaDialogue:
} }
self.active_dialogues[channel_id] = state self.active_dialogues[channel_id] = state
globals.LAST_PERSONA_DIALOGUE_TIME = time.time() 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 return state
def end_dialogue(self, channel_id: int): def end_dialogue(self, channel_id: int):
"""End a dialogue in a channel""" """End a dialogue in a channel"""
if channel_id in self.active_dialogues: if channel_id in self.active_dialogues:
state = self.active_dialogues[channel_id] state = self.active_dialogues[channel_id]
print(f"🏁 Ended persona dialogue in channel {channel_id}") logger.info(f"Ended persona dialogue in channel {channel_id}")
print(f" Turns: {state['turn_count']}, Final tension: {state['tension']:.2f}") logger.info(f" Turns: {state['turn_count']}, Final tension: {state['tension']:.2f}")
del self.active_dialogues[channel_id] del self.active_dialogues[channel_id]
# ======================================================================== # ========================================================================
@@ -400,7 +409,7 @@ class PersonaDialogue:
else: else:
base_delta = -sentiment_score * 0.05 base_delta = -sentiment_score * 0.05
except Exception as e: 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() 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 # Override: If the response contains a question mark, always continue
if '?' in response_text: 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 should_continue = True
if confidence == "LOW": if confidence == "LOW":
confidence = "MEDIUM" confidence = "MEDIUM"
@@ -605,12 +614,12 @@ You can use emojis naturally! ✨💙"""
# Safety limits # Safety limits
if state["turn_count"] >= MAX_TURNS: 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) self.end_dialogue(channel_id)
return return
if time.time() - state["started_at"] > DIALOGUE_TIMEOUT: 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) self.end_dialogue(channel_id)
return return
@@ -625,7 +634,7 @@ You can use emojis naturally! ✨💙"""
) )
if not response_text: 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) self.end_dialogue(channel_id)
return return
@@ -639,11 +648,11 @@ You can use emojis naturally! ✨💙"""
"total": state["tension"], "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 # Check if we should escalate to argument
if state["tension"] >= ARGUMENT_TENSION_THRESHOLD: 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 # Send the response that pushed us over
await self._send_as_persona(channel, responding_persona, response_text) await self._send_as_persona(channel, responding_persona, response_text)
@@ -659,7 +668,7 @@ You can use emojis naturally! ✨💙"""
state["turn_count"] += 1 state["turn_count"] += 1
state["last_speaker"] = responding_persona 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 # Decide what happens next
opposite = "evil" if responding_persona == "miku" else "miku" opposite = "evil" if responding_persona == "miku" else "miku"
@@ -677,14 +686,14 @@ You can use emojis naturally! ✨💙"""
) )
else: else:
# Clear signal to end # 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) self.end_dialogue(channel_id)
async def _next_turn(self, channel: discord.TextChannel, persona: str): async def _next_turn(self, channel: discord.TextChannel, persona: str):
"""Queue the next turn""" """Queue the next turn"""
# Check if dialogue was interrupted # Check if dialogue was interrupted
if await self._was_interrupted(channel): 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) self.end_dialogue(channel.id)
return return
@@ -741,7 +750,7 @@ Don't force a response if you have nothing meaningful to contribute."""
return return
if "[DONE]" in response.upper(): 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) self.end_dialogue(channel_id)
else: else:
clean_response = response.replace("[DONE]", "").strip() 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"]) tension_delta = self.calculate_tension_delta(clean_response, state["tension"])
state["tension"] = max(0.0, min(1.0, state["tension"] + tension_delta)) 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 # Check for argument escalation
if state["tension"] >= ARGUMENT_TENSION_THRESHOLD: 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._send_as_persona(channel, persona, clean_response)
await self._escalate_to_argument(channel, persona, clean_response) await self._escalate_to_argument(channel, persona, clean_response)
return return
@@ -782,7 +791,7 @@ Don't force a response if you have nothing meaningful to contribute."""
] ]
if all(closing_indicators): 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) self.end_dialogue(channel.id)
else: else:
asyncio.create_task(self._next_turn(channel, opposite)) 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 # Don't start if an argument is already going
if is_argument_in_progress(channel.id): 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 return
# Build context for the argument # 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.""" 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 # Use the existing argument system
# Pass the triggering message so the opposite persona responds to it # 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: if msg.author.id != globals.client.user.id:
return True return True
except Exception as e: except Exception as e:
print(f"⚠️ Error checking for interruption: {e}") logger.warning(f"Error checking for interruption: {e}")
return False return False
@@ -853,7 +862,7 @@ This pushed things over the edge into a full argument."""
messages.reverse() messages.reverse()
except Exception as e: except Exception as e:
print(f"⚠️ Error building conversation context: {e}") logger.warning(f"Error building conversation context: {e}")
return '\n'.join(messages) 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) webhooks = await get_or_create_webhooks_for_channel(channel)
if not webhooks: if not webhooks:
print(f"⚠️ Could not get webhooks for #{channel.name}") logger.warning(f"Could not get webhooks for #{channel.name}")
return return
webhook = webhooks["evil_miku"] if persona == "evil" else webhooks["miku"] 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: try:
await webhook.send(content=content, username=display_name) await webhook.send(content=content, username=display_name)
except Exception as e: 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: Returns:
True if an interjection was triggered, False otherwise 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() scorer = get_interjection_scorer()
dialogue_manager = get_dialogue_manager() dialogue_manager = get_dialogue_manager()
# Don't trigger if dialogue already active # Don't trigger if dialogue already active
if dialogue_manager.is_dialogue_active(message.channel.id): 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 return False
# Check if we should interject # Check if we should interject
should_interject, reason, score = await scorer.should_interject(message, current_persona) 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: if should_interject:
opposite_persona = "evil" if current_persona == "miku" else "miku" 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 # Start dialogue with the opposite persona responding first
dialogue_manager.start_dialogue(message.channel.id) dialogue_manager.start_dialogue(message.channel.id)

View File

@@ -25,8 +25,11 @@ import discord
import globals import globals
from .danbooru_client import danbooru_client from .danbooru_client import danbooru_client
from .logger import get_logger
import globals import globals
logger = get_logger('vision')
class ProfilePictureManager: class ProfilePictureManager:
"""Manages Miku's profile picture with intelligent cropping and face detection""" """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 aiohttp.ClientSession() as session:
async with session.get("http://anime-face-detector:6078/health", timeout=aiohttp.ClientTimeout(total=5)) as response: async with session.get("http://anime-face-detector:6078/health", timeout=aiohttp.ClientTimeout(total=5)) as response:
if response.status == 200: if response.status == 200:
print("Anime face detector API connected (pre-loaded)") logger.info("Anime face detector API connected (pre-loaded)")
return True return True
except Exception as e: 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 return False
async def _ensure_vram_available(self, debug: bool = False): async def _ensure_vram_available(self, debug: bool = False):
@@ -68,7 +71,7 @@ class ProfilePictureManager:
""" """
try: try:
if debug: 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 # Make a simple request to text model to trigger swap
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
@@ -86,13 +89,13 @@ class ProfilePictureManager:
) as response: ) as response:
if response.status == 200: if response.status == 200:
if debug: if debug:
print("Vision model unloaded, VRAM available") logger.debug("Vision model unloaded, VRAM available")
# Give system time to fully release VRAM # Give system time to fully release VRAM
await asyncio.sleep(3) await asyncio.sleep(3)
return True return True
except Exception as e: except Exception as e:
if debug: if debug:
print(f"⚠️ Could not swap models: {e}") logger.error(f"Could not swap models: {e}")
return False return False
@@ -100,7 +103,7 @@ class ProfilePictureManager:
"""Start the face detector container using Docker socket API""" """Start the face detector container using Docker socket API"""
try: try:
if debug: if debug:
print("🚀 Starting face detector container...") logger.info("Starting face detector container...")
# Use Docker socket API to start container # Use Docker socket API to start container
import aiofiles import aiofiles
@@ -112,7 +115,7 @@ class ProfilePictureManager:
# Check if socket exists # Check if socket exists
if not os.path.exists(socket_path): if not os.path.exists(socket_path):
if debug: if debug:
print("⚠️ Docker socket not available") logger.error("Docker socket not available")
return False return False
# Use aiohttp UnixConnector to communicate with Docker socket # 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 response.status not in [204, 304]: # 204=started, 304=already running
if debug: if debug:
error_text = await response.text() 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 return False
# Wait for API to be ready # Wait for API to be ready
@@ -140,32 +143,32 @@ class ProfilePictureManager:
) as response: ) as response:
if response.status == 200: if response.status == 200:
if debug: if debug:
print(f"Face detector ready (took {i+1}s)") logger.info(f"Face detector ready (took {i+1}s)")
return True return True
except: except:
pass pass
await asyncio.sleep(1) await asyncio.sleep(1)
if debug: if debug:
print("⚠️ Face detector didn't become ready in time") logger.warning("Face detector didn't become ready in time")
return False return False
except Exception as e: except Exception as e:
if debug: if debug:
print(f"⚠️ Error starting face detector: {e}") logger.error(f"Error starting face detector: {e}")
return False return False
async def _stop_face_detector(self, debug: bool = False): async def _stop_face_detector(self, debug: bool = False):
"""Stop the face detector container using Docker socket API""" """Stop the face detector container using Docker socket API"""
try: try:
if debug: if debug:
print("🛑 Stopping face detector to free VRAM...") logger.info("Stopping face detector to free VRAM...")
socket_path = "/var/run/docker.sock" socket_path = "/var/run/docker.sock"
if not os.path.exists(socket_path): if not os.path.exists(socket_path):
if debug: if debug:
print("⚠️ Docker socket not available") logger.error("Docker socket not available")
return return
from aiohttp import UnixConnector from aiohttp import UnixConnector
@@ -178,26 +181,26 @@ class ProfilePictureManager:
async with session.post(url, params={"t": 10}) as response: # 10 second timeout 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 response.status in [204, 304]: # 204=stopped, 304=already stopped
if debug: if debug:
print("Face detector stopped") logger.info("Face detector stopped")
else: else:
if debug: if debug:
error_text = await response.text() 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: except Exception as e:
if debug: 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): async def save_current_avatar_as_fallback(self):
"""Save the bot's current avatar as fallback (only if fallback doesn't exist)""" """Save the bot's current avatar as fallback (only if fallback doesn't exist)"""
try: try:
# Only save if fallback doesn't already exist # Only save if fallback doesn't already exist
if os.path.exists(self.FALLBACK_PATH): if os.path.exists(self.FALLBACK_PATH):
print("Fallback avatar already exists, skipping save") logger.info("Fallback avatar already exists, skipping save")
return True return True
if not globals.client or not globals.client.user: if not globals.client or not globals.client.user:
print("⚠️ Bot client not ready") logger.warning("Bot client not ready")
return False return False
avatar_asset = globals.client.user.avatar or globals.client.user.default_avatar 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: with open(self.FALLBACK_PATH, 'wb') as f:
f.write(avatar_bytes) 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 return True
except Exception as e: except Exception as e:
print(f"⚠️ Error saving fallback avatar: {e}") logger.error(f"Error saving fallback avatar: {e}")
return False return False
async def change_profile_picture( async def change_profile_picture(
@@ -251,7 +254,7 @@ class ProfilePictureManager:
if custom_image_bytes: if custom_image_bytes:
# Custom upload - no retry needed # Custom upload - no retry needed
if debug: if debug:
print("🖼️ Using provided custom image") logger.info("Using provided custom image")
image_bytes = custom_image_bytes image_bytes = custom_image_bytes
result["source"] = "custom_upload" result["source"] = "custom_upload"
@@ -259,7 +262,7 @@ class ProfilePictureManager:
try: try:
image = Image.open(io.BytesIO(image_bytes)) image = Image.open(io.BytesIO(image_bytes))
if debug: if debug:
print(f"📐 Original image size: {image.size}") logger.debug(f"Original image size: {image.size}")
# Check if it's an animated GIF # Check if it's an animated GIF
if image.format == 'GIF': if image.format == 'GIF':
@@ -269,11 +272,11 @@ class ProfilePictureManager:
is_animated_gif = True is_animated_gif = True
image.seek(0) # Reset to first frame image.seek(0) # Reset to first frame
if debug: if debug:
print("🎬 Detected animated GIF - will preserve animation") logger.debug("Detected animated GIF - will preserve animation")
except EOFError: except EOFError:
# Only one frame, treat as static image # Only one frame, treat as static image
if debug: 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: except Exception as e:
result["error"] = f"Failed to open image: {e}" result["error"] = f"Failed to open image: {e}"
@@ -282,11 +285,11 @@ class ProfilePictureManager:
else: else:
# Danbooru - retry until we find a valid Miku image # Danbooru - retry until we find a valid Miku image
if debug: 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): for attempt in range(max_retries):
if attempt > 0 and debug: 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) post = await danbooru_client.get_random_miku_image(mood=mood)
if not post: if not post:
@@ -302,23 +305,23 @@ class ProfilePictureManager:
continue continue
if debug: 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 # Load image with PIL
try: try:
temp_image = Image.open(io.BytesIO(temp_image_bytes)) temp_image = Image.open(io.BytesIO(temp_image_bytes))
if debug: if debug:
print(f"📐 Original image size: {temp_image.size}") logger.debug(f"Original image size: {temp_image.size}")
except Exception as e: except Exception as e:
if debug: if debug:
print(f"⚠️ Failed to open image: {e}") logger.warning(f"Failed to open image: {e}")
continue continue
# Verify it's Miku # Verify it's Miku
miku_verification = await self._verify_and_locate_miku(temp_image_bytes, debug=debug) miku_verification = await self._verify_and_locate_miku(temp_image_bytes, debug=debug)
if not miku_verification["is_miku"]: if not miku_verification["is_miku"]:
if debug: if debug:
print(f"Image verification failed: not Miku, trying another...") logger.warning(f"Image verification failed: not Miku, trying another...")
continue continue
# Success! This image is valid # Success! This image is valid
@@ -330,7 +333,7 @@ class ProfilePictureManager:
# If multiple characters detected, use LLM's suggested crop region # If multiple characters detected, use LLM's suggested crop region
if miku_verification.get("crop_region"): if miku_verification.get("crop_region"):
if debug: 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"]) image = self._apply_crop_region(image, miku_verification["crop_region"])
break break
@@ -344,11 +347,11 @@ class ProfilePictureManager:
# If this is an animated GIF, skip most processing and use raw bytes # If this is an animated GIF, skip most processing and use raw bytes
if is_animated_gif: if is_animated_gif:
if debug: 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 # Generate description of the animated GIF
if debug: 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) description = await self._generate_gif_description(image_bytes, debug=debug)
if description: if description:
# Save description to file # Save description to file
@@ -358,12 +361,12 @@ class ProfilePictureManager:
f.write(description) f.write(description)
result["metadata"]["description"] = description result["metadata"]["description"] = description
if debug: if debug:
print(f"📝 Saved GIF description ({len(description)} chars)") logger.info(f"Saved GIF description ({len(description)} chars)")
except Exception as e: except Exception as e:
print(f"⚠️ Failed to save description file: {e}") logger.error(f"Failed to save description file: {e}")
else: else:
if debug: if debug:
print("⚠️ GIF description generation returned None") logger.error("GIF description generation returned None")
# Extract dominant color from first frame # Extract dominant color from first frame
dominant_color = self._extract_dominant_color(image, debug=debug) dominant_color = self._extract_dominant_color(image, debug=debug)
@@ -373,14 +376,14 @@ class ProfilePictureManager:
"hex": "#{:02x}{:02x}{:02x}".format(*dominant_color) "hex": "#{:02x}{:02x}{:02x}".format(*dominant_color)
} }
if debug: 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 # Save the original GIF bytes
with open(self.CURRENT_PATH, 'wb') as f: with open(self.CURRENT_PATH, 'wb') as f:
f.write(image_bytes) f.write(image_bytes)
if debug: 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 # Update Discord avatar with original GIF
if globals.client and globals.client.user: if globals.client and globals.client.user:
@@ -401,7 +404,7 @@ class ProfilePictureManager:
# Save metadata # Save metadata
self._save_metadata(result["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 # Update role colors if we have a dominant color
if dominant_color: if dominant_color:
@@ -411,12 +414,13 @@ class ProfilePictureManager:
except discord.HTTPException as e: except discord.HTTPException as e:
result["error"] = f"Discord API error: {e}" result["error"] = f"Discord API error: {e}"
print(f"⚠️ Failed to update Discord avatar with GIF: {e}") logger.warning(f"Failed to update Discord avatar with GIF: {e}")
print(f" Note: Animated avatars require Discord Nitro") if debug:
logger.debug("Note: Animated avatars require Discord Nitro")
return result return result
except Exception as e: except Exception as e:
result["error"] = f"Unexpected error updating avatar: {e}" result["error"] = f"Unexpected error updating avatar: {e}"
print(f"⚠️ Unexpected error: {e}") logger.error(f"Unexpected error: {e}")
return result return result
else: else:
result["error"] = "Bot client not ready" result["error"] = "Bot client not ready"
@@ -425,7 +429,7 @@ class ProfilePictureManager:
# === NORMAL STATIC IMAGE PATH === # === NORMAL STATIC IMAGE PATH ===
# Step 2: Generate description of the validated image # Step 2: Generate description of the validated image
if debug: if debug:
print("📝 Generating image description...") logger.info("Generating image description...")
description = await self._generate_image_description(image_bytes, debug=debug) description = await self._generate_image_description(image_bytes, debug=debug)
if description: if description:
# Save description to file # Save description to file
@@ -435,12 +439,12 @@ class ProfilePictureManager:
f.write(description) f.write(description)
result["metadata"]["description"] = description result["metadata"]["description"] = description
if debug: if debug:
print(f"📝 Saved image description ({len(description)} chars)") logger.info(f"Saved image description ({len(description)} chars)")
except Exception as e: except Exception as e:
print(f"⚠️ Failed to save description file: {e}") logger.warning(f"Failed to save description file: {e}")
else: else:
if debug: if debug:
print("⚠️ Description generation returned None") logger.warning("Description generation returned None")
# Step 3: Detect face and crop intelligently # Step 3: Detect face and crop intelligently
cropped_image = await self._intelligent_crop(image, image_bytes, target_size=512, debug=debug) cropped_image = await self._intelligent_crop(image, image_bytes, target_size=512, debug=debug)
@@ -459,7 +463,7 @@ class ProfilePictureManager:
f.write(cropped_bytes) f.write(cropped_bytes)
if debug: 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 # Step 5: Extract dominant color from saved current.png
saved_image = Image.open(self.CURRENT_PATH) saved_image = Image.open(self.CURRENT_PATH)
@@ -470,7 +474,7 @@ class ProfilePictureManager:
"hex": "#{:02x}{:02x}{:02x}".format(*dominant_color) "hex": "#{:02x}{:02x}{:02x}".format(*dominant_color)
} }
if debug: 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 # Step 6: Update Discord avatar
if globals.client and globals.client.user: if globals.client and globals.client.user:
@@ -495,7 +499,7 @@ class ProfilePictureManager:
# Save metadata # Save metadata
self._save_metadata(result["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 # Step 7: Update role colors across all servers
if dominant_color: if dominant_color:
@@ -503,16 +507,16 @@ class ProfilePictureManager:
except discord.HTTPException as e: except discord.HTTPException as e:
result["error"] = f"Discord API error: {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: except Exception as e:
result["error"] = f"Unexpected error updating avatar: {e}" result["error"] = f"Unexpected error updating avatar: {e}"
print(f"⚠️ Unexpected error: {e}") logger.error(f"Unexpected error: {e}")
else: else:
result["error"] = "Bot client not ready" result["error"] = "Bot client not ready"
except Exception as e: except Exception as e:
result["error"] = f"Unexpected error: {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 return result
@@ -524,7 +528,7 @@ class ProfilePictureManager:
if response.status == 200: if response.status == 200:
return await response.read() return await response.read()
except Exception as e: except Exception as e:
print(f"⚠️ Error downloading image: {e}") logger.error(f"Error downloading image: {e}")
return None return None
async def _generate_image_description(self, image_bytes: bytes, debug: bool = False) -> Optional[str]: 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') image_b64 = base64.b64encode(image_bytes).decode('utf-8')
if debug: 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. prompt = """This is an image of Hatsune Miku that will be used as a profile picture.
Please describe this image in detail, including: 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"} headers = {"Content-Type": "application/json"}
if debug: 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 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: 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() data = await resp.json()
if debug: if debug:
print(f"📦 API Response keys: {data.keys()}") logger.debug(f"API Response keys: {data.keys()}")
print(f"📦 Choices: {data.get('choices', [])}") logger.debug(f"Choices: {data.get('choices', [])}")
# Try to get content from the response # Try to get content from the response
choice = data.get("choices", [{}])[0] 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 description and description.strip():
if debug: if debug:
print(f"Generated description: {description[:100]}...") logger.info(f"Generated description: {description[:100]}...")
return description.strip() return description.strip()
else: else:
if debug: if debug:
print(f"⚠️ Description is empty or None") logger.warning(f"Description is empty or None")
print(f" Full response: {data}") logger.warning(f" Full response: {data}")
else: else:
print(f"⚠️ Description is empty or None") logger.warning(f"Description is empty or None")
return None return None
else: else:
error_text = await resp.text() 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: except Exception as e:
print(f"⚠️ Error generating image description: {e}") logger.error(f"Error generating image description: {e}")
import traceback import traceback
traceback.print_exc() 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 from utils.image_handling import extract_video_frames, analyze_video_with_vision
if debug: if debug:
print("🎬 Extracting frames from GIF...") logger.info("Extracting frames from GIF...")
# Extract frames from the GIF (6 frames for good analysis) # Extract frames from the GIF (6 frames for good analysis)
frames = await extract_video_frames(gif_bytes, num_frames=6) frames = await extract_video_frames(gif_bytes, num_frames=6)
if not frames: if not frames:
if debug: if debug:
print("⚠️ Failed to extract frames from GIF") logger.warning("Failed to extract frames from GIF")
return None return None
if debug: if debug:
print(f"Extracted {len(frames)} frames from GIF") logger.info(f"Extracted {len(frames)} frames from GIF")
print(f"🌐 Analyzing GIF with vision model...") logger.info(f"Analyzing GIF with vision model...")
# Use the existing analyze_video_with_vision function (no timeout issues) # Use the existing analyze_video_with_vision function (no timeout issues)
# Note: This uses a generic prompt, but it works reliably # 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 description and description.strip() and not description.startswith("Error"):
if debug: if debug:
print(f"Generated GIF description: {description[:100]}...") logger.info(f"Generated GIF description: {description[:100]}...")
return description.strip() return description.strip()
else: else:
if debug: if debug:
print(f"⚠️ GIF description failed or empty: {description}") logger.warning(f"GIF description failed or empty: {description}")
return None return None
except Exception as e: except Exception as e:
print(f"⚠️ Error generating GIF description: {e}") logger.error(f"Error generating GIF description: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
@@ -740,11 +744,11 @@ Respond in JSON format:
response = data.get("choices", [{}])[0].get("message", {}).get("content", "") response = data.get("choices", [{}])[0].get("message", {}).get("content", "")
else: else:
error_text = await resp.text() 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 return result
if debug: if debug:
print(f"🤖 Vision model response: {response}") logger.debug(f"Vision model response: {response}")
# Parse JSON response # Parse JSON response
import re import re
@@ -766,7 +770,7 @@ Respond in JSON format:
result["is_miku"] = "yes" in response_lower or "miku" in response_lower result["is_miku"] = "yes" in response_lower or "miku" in response_lower
except Exception as e: 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) # Assume it's Miku on error (trust Danbooru tags)
result["is_miku"] = True result["is_miku"] = True
@@ -793,7 +797,7 @@ Respond in JSON format:
region["vertical"] = "bottom" region["vertical"] = "bottom"
if debug: if debug:
print(f"📍 Parsed location '{location}' -> {region}") logger.debug(f"Parsed location '{location}' -> {region}")
return region return region
@@ -856,11 +860,11 @@ Respond in JSON format:
if face_detection and face_detection.get('center'): if face_detection and face_detection.get('center'):
if debug: if debug:
print(f"😊 Face detected at {face_detection['center']}") logger.debug(f"Face detected at {face_detection['center']}")
crop_center = face_detection['center'] crop_center = face_detection['center']
else: else:
if debug: if debug:
print("🎯 No face detected, using saliency detection") logger.debug("No face detected, using saliency detection")
# Fallback to saliency detection # Fallback to saliency detection
cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
crop_center = self._detect_saliency(cv_image, debug=debug) crop_center = self._detect_saliency(cv_image, debug=debug)
@@ -895,12 +899,12 @@ Respond in JSON format:
top = 0 top = 0
# Adjust crop_center for logging # Adjust crop_center for logging
if debug: 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: elif top + crop_size > height:
# Face is too close to bottom edge # Face is too close to bottom edge
top = height - crop_size top = height - crop_size
if debug: 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 # Crop
cropped = image.crop((left, top, left + crop_size, top + crop_size)) 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) cropped = cropped.resize((target_size, target_size), Image.Resampling.LANCZOS)
if debug: 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 return cropped
@@ -933,7 +937,7 @@ Respond in JSON format:
# Step 2: Start face detector container # Step 2: Start face detector container
if not await self._start_face_detector(debug=debug): if not await self._start_face_detector(debug=debug):
if debug: if debug:
print("⚠️ Could not start face detector") logger.error("Could not start face detector")
return None return None
face_detector_started = True face_detector_started = True
@@ -951,14 +955,14 @@ Respond in JSON format:
) as response: ) as response:
if response.status != 200: if response.status != 200:
if debug: if debug:
print(f"⚠️ Face detection API returned status {response.status}") logger.error(f"Face detection API returned status {response.status}")
return None return None
result = await response.json() result = await response.json()
if result.get('count', 0) == 0: if result.get('count', 0) == 0:
if debug: if debug:
print("👤 No faces detected by API") logger.debug("No faces detected by API")
return None return None
# Get detections and pick the one with highest confidence # Get detections and pick the one with highest confidence
@@ -981,9 +985,9 @@ Respond in JSON format:
if debug: if debug:
width = int(x2 - x1) width = int(x2 - x1)
height = int(y2 - y1) height = int(y2 - y1)
print(f"👤 Detected {len(detections)} face(s) via API, using best at ({center_x}, {center_y}) [confidence: {confidence:.2%}]") logger.debug(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}") logger.debug(f" Bounding box: x={int(x1)}, y={int(y1)}, w={width}, h={height}")
print(f" Keypoints: {len(keypoints)} facial landmarks detected") logger.debug(f" Keypoints: {len(keypoints)} facial landmarks detected")
return { return {
'center': (center_x, center_y), 'center': (center_x, center_y),
@@ -995,10 +999,10 @@ Respond in JSON format:
except asyncio.TimeoutError: except asyncio.TimeoutError:
if debug: if debug:
print("⚠️ Face detection API timeout") logger.warning("Face detection API timeout")
except Exception as e: except Exception as e:
if debug: if debug:
print(f"⚠️ Error calling face detection API: {e}") logger.error(f"Error calling face detection API: {e}")
finally: finally:
# Always stop face detector to free VRAM # Always stop face detector to free VRAM
if face_detector_started: if face_detector_started:
@@ -1027,12 +1031,12 @@ Respond in JSON format:
_, max_val, _, max_loc = cv2.minMaxLoc(saliency_map) _, max_val, _, max_loc = cv2.minMaxLoc(saliency_map)
if debug: if debug:
print(f"🎯 Saliency peak at {max_loc}") logger.debug(f"Saliency peak at {max_loc}")
return max_loc return max_loc
except Exception as e: except Exception as e:
if debug: if debug:
print(f"⚠️ Saliency detection failed: {e}") logger.error(f"Saliency detection failed: {e}")
# Ultimate fallback: center of image # Ultimate fallback: center of image
height, width = cv_image.shape[:2] height, width = cv_image.shape[:2]
@@ -1070,7 +1074,7 @@ Respond in JSON format:
if len(pixels) == 0: if len(pixels) == 0:
if debug: 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 return (200, 200, 200) # Neutral gray fallback
# Use k-means to find dominant colors # Use k-means to find dominant colors
@@ -1085,11 +1089,11 @@ Respond in JSON format:
counts = np.bincount(labels) counts = np.bincount(labels)
if debug: 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)): for i, (color, count) in enumerate(zip(colors, counts)):
pct = (count / len(labels)) * 100 pct = (count / len(labels)) * 100
r, g, b = color.astype(int) 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 # Sort by frequency
sorted_indices = np.argsort(-counts) 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 saturation = (max_c - min_c) / max_c if max_c > 0 else 0
if debug: 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 # Prefer more saturated colors
if saturation > best_saturation: if saturation > best_saturation:
@@ -1118,7 +1122,7 @@ Respond in JSON format:
if best_color: if best_color:
if debug: 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 return best_color
# Fallback to most common color # Fallback to most common color
@@ -1126,12 +1130,12 @@ Respond in JSON format:
# Convert to native Python ints # Convert to native Python ints
result = (int(dominant_color[0]), int(dominant_color[1]), int(dominant_color[2])) result = (int(dominant_color[0]), int(dominant_color[1]), int(dominant_color[2]))
if debug: if debug:
print(f"🎨 Using most common color: RGB{result}") logger.debug(f"Using most common color: RGB{result}")
return result return result
except Exception as e: except Exception as e:
if debug: if debug:
print(f"⚠️ Error extracting dominant color: {e}") logger.error(f"Error extracting dominant color: {e}")
return None return None
async def _update_role_colors(self, color: Tuple[int, int, int], debug: bool = False): 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 debug: Enable debug output
""" """
if debug: 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 not globals.client:
if debug: if debug:
print("⚠️ No client available for role updates") logger.error("No client available for role updates")
return return
if debug: 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) # Convert RGB to Discord color (integer)
discord_color = discord.Color.from_rgb(*color) discord_color = discord.Color.from_rgb(*color)
@@ -1162,20 +1166,20 @@ Respond in JSON format:
for guild in globals.client.guilds: for guild in globals.client.guilds:
try: try:
if debug: if debug:
print(f"🔍 Checking guild: {guild.name}") logger.debug(f"Checking guild: {guild.name}")
# Find the bot's top role (usually colored role) # Find the bot's top role (usually colored role)
member = guild.get_member(globals.client.user.id) member = guild.get_member(globals.client.user.id)
if not member: if not member:
if debug: if debug:
print(f" ⚠️ Bot not found as member in {guild.name}") logger.warning(f" Bot not found as member in {guild.name}")
continue continue
# Get the highest role that the bot has (excluding @everyone) # Get the highest role that the bot has (excluding @everyone)
roles = [r for r in member.roles if r.name != "@everyone"] roles = [r for r in member.roles if r.name != "@everyone"]
if not roles: if not roles:
if debug: if debug:
print(f" ⚠️ No roles found in {guild.name}") logger.warning(f" No roles found in {guild.name}")
continue continue
# Look for a dedicated color role first (e.g., "Miku Color") # 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 # Use dedicated color role if found, otherwise use top role
if color_role: if color_role:
if debug: 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 target_role = color_role
else: else:
if debug: 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 target_role = bot_top_role
# Check permissions # Check permissions
can_manage = guild.me.guild_permissions.manage_roles can_manage = guild.me.guild_permissions.manage_roles
if debug: if debug:
print(f" 🔑 Manage roles permission: {can_manage}") logger.debug(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" 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 # Only update if we have permission and it's not a special role
if can_manage: if can_manage:
@@ -1219,28 +1223,28 @@ Respond in JSON format:
updated_count += 1 updated_count += 1
if debug: 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: else:
if debug: if debug:
print(f" ⚠️ No manage_roles permission in {guild.name}") logger.warning(f" No manage_roles permission in {guild.name}")
except discord.Forbidden: except discord.Forbidden:
failed_count += 1 failed_count += 1
if debug: 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: except Exception as e:
failed_count += 1 failed_count += 1
if debug: if debug:
print(f" Error updating role in {guild.name}: {e}") logger.error(f" Error updating role in {guild.name}: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
if updated_count > 0: 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: 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: 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: async def set_custom_role_color(self, hex_color: str, debug: bool = False) -> Dict:
""" """
@@ -1267,7 +1271,7 @@ Respond in JSON format:
} }
if debug: 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) await self._update_role_colors(color, debug=debug)
@@ -1290,7 +1294,7 @@ Respond in JSON format:
Dict with success status Dict with success status
""" """
if debug: 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) 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: with open(self.METADATA_PATH, 'w') as f:
json.dump(metadata, f, indent=2) json.dump(metadata, f, indent=2)
except Exception as e: except Exception as e:
print(f"⚠️ Error saving metadata: {e}") logger.error(f"Error saving metadata: {e}")
def load_metadata(self) -> Optional[Dict]: def load_metadata(self) -> Optional[Dict]:
"""Load metadata about current profile picture""" """Load metadata about current profile picture"""
@@ -1317,14 +1321,14 @@ Respond in JSON format:
with open(self.METADATA_PATH, 'r') as f: with open(self.METADATA_PATH, 'r') as f:
return json.load(f) return json.load(f)
except Exception as e: except Exception as e:
print(f"⚠️ Error loading metadata: {e}") logger.error(f"Error loading metadata: {e}")
return None return None
async def restore_fallback(self) -> bool: async def restore_fallback(self) -> bool:
"""Restore the fallback profile picture""" """Restore the fallback profile picture"""
try: try:
if not os.path.exists(self.FALLBACK_PATH): if not os.path.exists(self.FALLBACK_PATH):
print("⚠️ No fallback avatar found") logger.warning("No fallback avatar found")
return False return False
with open(self.FALLBACK_PATH, 'rb') as f: with open(self.FALLBACK_PATH, 'rb') as f:
@@ -1341,11 +1345,11 @@ Respond in JSON format:
else: else:
await globals.client.user.edit(avatar=avatar_bytes) await globals.client.user.edit(avatar=avatar_bytes)
print("Restored fallback avatar") logger.info("Restored fallback avatar")
return True return True
except Exception as e: except Exception as e:
print(f"⚠️ Error restoring fallback: {e}") logger.error(f"Error restoring fallback: {e}")
return False return False
@@ -1362,7 +1366,7 @@ Respond in JSON format:
with open(description_path, 'r', encoding='utf-8') as f: with open(description_path, 'r', encoding='utf-8') as f:
return f.read().strip() return f.read().strip()
except Exception as e: except Exception as e:
print(f"⚠️ Error reading description: {e}") logger.error(f"Error reading description: {e}")
return None return None

View File

@@ -13,6 +13,9 @@ import globals
from server_manager import server_manager from server_manager import server_manager
from utils.llm import query_llama from utils.llm import query_llama
from utils.dm_interaction_analyzer import dm_analyzer 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" 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""" """Send Monday video for a specific server"""
server_config = server_manager.get_server_config(guild_id) server_config = server_manager.get_server_config(guild_id)
if not server_config: if not server_config:
print(f"⚠️ No config found for server {guild_id}") logger.warning(f"No config found for server {guild_id}")
return return
# No need to switch model - llama-swap handles this automatically # 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: for channel_id in target_channel_ids:
channel = globals.client.get_channel(channel_id) channel = globals.client.get_channel(channel_id)
if channel is None: 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 continue
try: try:
@@ -45,9 +48,9 @@ async def send_monday_video_for_server(guild_id: int):
# Send video link # Send video link
await channel.send(f"[Happy Miku Monday!]({video_url})") 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: 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(): async def send_monday_video():
"""Legacy function - now sends to all servers""" """Legacy function - now sends to all servers"""
@@ -61,7 +64,7 @@ def load_last_bedtime_targets():
with open(BEDTIME_TRACKING_FILE, "r") as f: with open(BEDTIME_TRACKING_FILE, "r") as f:
return json.load(f) return json.load(f)
except Exception as e: except Exception as e:
print(f"⚠️ Failed to load bedtime tracking file: {e}") logger.error(f"Failed to load bedtime tracking file: {e}")
return {} return {}
_last_bedtime_targets = load_last_bedtime_targets() _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: with open(BEDTIME_TRACKING_FILE, "w") as f:
json.dump(data, f) json.dump(data, f)
except Exception as e: 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): async def send_bedtime_reminder_for_server(guild_id: int, client=None):
"""Send bedtime reminder for a specific server""" """Send bedtime reminder for a specific server"""
server_config = server_manager.get_server_config(guild_id) server_config = server_manager.get_server_config(guild_id)
if not server_config: if not server_config:
print(f"⚠️ No config found for server {guild_id}") logger.warning(f"No config found for server {guild_id}")
return return
# Use provided client or fall back to globals.client # 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 client = globals.client
if client is None: 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 return
# No need to switch model - llama-swap handles this automatically # 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: for channel_id in server_config.bedtime_channel_ids:
channel = client.get_channel(channel_id) channel = client.get_channel(channel_id)
if not channel: 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 continue
guild = channel.guild guild = channel.guild
@@ -112,7 +115,8 @@ async def send_bedtime_reminder_for_server(guild_id: int, client=None):
online_members.append(specific_user) online_members.append(specific_user)
if not online_members: 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 continue
# Avoid repeating the same person unless they're the only one # 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: try:
await channel.send(f"{chosen_one.mention} {bedtime_message}") 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: 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(): async def send_bedtime_reminder():
"""Legacy function - now sends to all servers""" """Legacy function - now sends to all servers"""
@@ -176,7 +180,7 @@ def schedule_random_bedtime():
for guild_id in server_manager.servers: for guild_id in server_manager.servers:
# Schedule bedtime for each server using the async function # Schedule bedtime for each server using the async function
# This will be called from the server manager's event loop # 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 # Note: This function is now called from the server manager's context
# which properly handles the async operations # which properly handles the async operations
@@ -188,8 +192,8 @@ async def send_bedtime_now():
async def run_daily_dm_analysis(): async def run_daily_dm_analysis():
"""Run daily DM interaction analysis - reports one user per day""" """Run daily DM interaction analysis - reports one user per day"""
if dm_analyzer is None: if dm_analyzer is None:
print("⚠️ DM Analyzer not initialized, skipping daily analysis") logger.warning("DM Analyzer not initialized, skipping daily analysis")
return return
print("📊 Running daily DM interaction analysis...") logger.info("Running daily DM interaction analysis...")
await dm_analyzer.run_daily_analysis() await dm_analyzer.run_daily_analysis()

View File

@@ -1,4 +1,7 @@
from utils.llm import query_llama 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]: async def analyze_sentiment(messages: list) -> tuple[str, float]:
""" """
@@ -40,5 +43,5 @@ Response:"""
return summary, score return summary, score
except Exception as e: 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 return "Error analyzing sentiment", 0.5

View File

@@ -11,11 +11,14 @@ apply_twscrape_fix()
from twscrape import API, gather, Account from twscrape import API, gather, Account
from playwright.async_api import async_playwright from playwright.async_api import async_playwright
from pathlib import Path from pathlib import Path
from utils.logger import get_logger
logger = get_logger('media')
COOKIE_PATH = Path(__file__).parent / "x.com.cookies.json" COOKIE_PATH = Path(__file__).parent / "x.com.cookies.json"
async def extract_media_urls(page, tweet_url): async def extract_media_urls(page, tweet_url):
print(f"🔍 Visiting tweet page: {tweet_url}") logger.debug(f"Visiting tweet page: {tweet_url}")
try: try:
await page.goto(tweet_url, timeout=15000) await page.goto(tweet_url, timeout=15000)
await page.wait_for_timeout(1000) 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" cleaned = src.split("&name=")[0] + "&name=large"
urls.add(cleaned) 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) return list(urls)
except Exception as e: except Exception as e:
print(f"Playwright error on {tweet_url}: {e}") logger.error(f"Playwright error on {tweet_url}: {e}")
return [] return []
async def fetch_miku_tweets(limit=5): async def fetch_miku_tweets(limit=5):
@@ -53,11 +56,11 @@ async def fetch_miku_tweets(limit=5):
) )
await api.pool.login_all() 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' query = 'Hatsune Miku OR 初音ミク has:images after:2025'
tweets = await gather(api.search(query, limit=limit, kv={"product": "Top"})) 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: async with async_playwright() as p:
browser = await p.firefox.launch(headless=True) 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): for i, tweet in enumerate(tweets, 1):
username = tweet.user.username username = tweet.user.username
tweet_url = f"https://twitter.com/{username}/status/{tweet.id}" 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) media_urls = await extract_media_urls(page, tweet_url)
if media_urls: if media_urls:
@@ -90,7 +93,7 @@ async def fetch_miku_tweets(limit=5):
}) })
await browser.close() 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 return results
@@ -99,7 +102,7 @@ async def _search_latest(api: API, query: str, limit: int) -> list:
try: try:
return await gather(api.search(query, limit=limit, kv={"product": "Latest"})) return await gather(api.search(query, limit=limit, kv={"product": "Latest"}))
except Exception as e: except Exception as e:
print(f"⚠️ Latest search failed for '{query}': {e}") logger.error(f"Latest search failed for '{query}': {e}")
return [] return []
@@ -131,13 +134,13 @@ async def fetch_figurine_tweets_latest(limit_per_source: int = 10) -> list:
"miku from:OtakuOwletMerch", "miku from:OtakuOwletMerch",
] ]
print("🔎 Searching figurine tweets by Latest across sources...") logger.info("Searching figurine tweets by Latest across sources...")
all_tweets = [] all_tweets = []
for q in queries: for q in queries:
tweets = await _search_latest(api, q, limit_per_source) tweets = await _search_latest(api, q, limit_per_source)
all_tweets.extend(tweets) 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: async with async_playwright() as p:
browser = await p.firefox.launch(headless=True) browser = await p.firefox.launch(headless=True)
@@ -157,7 +160,7 @@ async def fetch_figurine_tweets_latest(limit_per_source: int = 10) -> list:
try: try:
username = tweet.user.username username = tweet.user.username
tweet_url = f"https://twitter.com/{username}/status/{tweet.id}" 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) media_urls = await extract_media_urls(page, tweet_url)
if media_urls: if media_urls:
results.append({ results.append({
@@ -167,10 +170,10 @@ async def fetch_figurine_tweets_latest(limit_per_source: int = 10) -> list:
"media": media_urls "media": media_urls
}) })
except Exception as e: except Exception as e:
print(f"⚠️ Error processing tweet: {e}") logger.error(f"Error processing tweet: {e}")
await browser.close() 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 return results

View File

@@ -7,6 +7,9 @@ See: https://github.com/vladkens/twscrape/issues/284
import json import json
import re import re
from utils.logger import get_logger
logger = get_logger('core')
def script_url(k: str, v: str): def script_url(k: str, v: str):
@@ -36,6 +39,6 @@ def apply_twscrape_fix():
try: try:
from twscrape import xclid from twscrape import xclid
xclid.get_scripts_list = patched_get_scripts_list 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: except Exception as e:
print(f"⚠️ Failed to apply twscrape monkey patch: {e}") logger.error(f"Failed to apply twscrape monkey patch: {e}")