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
)
from utils.dm_logger import dm_logger
from utils.logger import get_logger, list_components, get_component_stats
from utils.log_config import (
load_config as load_log_config,
save_config as save_log_config,
update_component,
update_global_level,
update_timestamp_format,
update_api_filters,
reset_to_defaults,
reload_all_loggers
)
import time
from fnmatch import fnmatch
nest_asyncio.apply()
# Initialize API logger
logger = get_logger('api')
api_requests_logger = get_logger('api.requests')
# ========== GPU Selection Helper ==========
def get_current_gpu_url():
"""Get the URL for the currently selected GPU"""
@@ -70,6 +87,58 @@ def get_current_gpu_url():
app = FastAPI()
# ========== Logging Middleware ==========
@app.middleware("http")
async def log_requests(request: Request, call_next):
"""Middleware to log HTTP requests based on configuration."""
start_time = time.time()
# Get logging config
log_config = load_log_config()
api_config = log_config.get('components', {}).get('api.requests', {})
# Check if API request logging is enabled
if not api_config.get('enabled', False):
return await call_next(request)
# Get filters
filters = api_config.get('filters', {})
exclude_paths = filters.get('exclude_paths', [])
exclude_status = filters.get('exclude_status', [])
include_slow_requests = filters.get('include_slow_requests', True)
slow_threshold_ms = filters.get('slow_threshold_ms', 1000)
# Process request
response = await call_next(request)
# Calculate duration
duration_ms = (time.time() - start_time) * 1000
# Check if path should be excluded
path = request.url.path
for pattern in exclude_paths:
if fnmatch(path, pattern):
return response
# Check if status should be excluded (unless it's a slow request)
is_slow = duration_ms >= slow_threshold_ms
if response.status_code in exclude_status and not (include_slow_requests and is_slow):
return response
# Log the request
log_msg = f"{request.method} {path} - {response.status_code} ({duration_ms:.2f}ms)"
if is_slow:
api_requests_logger.warning(f"SLOW REQUEST: {log_msg}")
elif response.status_code >= 500:
api_requests_logger.error(log_msg)
elif response.status_code >= 400:
api_requests_logger.warning(log_msg)
else:
api_requests_logger.api(log_msg)
return response
# Serve static folder
app.mount("/static", StaticFiles(directory="static"), name="static")
@@ -354,7 +423,7 @@ def trigger_argument(data: BipolarTriggerRequest):
channel_id, message_id, globals.client, data.context
)
if not success:
print(f"⚠️ Failed to trigger argument from message: {error}")
logger.error(f"Failed to trigger argument from message: {error}")
globals.client.loop.create_task(trigger_from_message())
@@ -419,17 +488,17 @@ def trigger_dialogue(data: dict):
continue
if not message:
print(f"⚠️ Message {message_id} not found")
logger.error(f"Message {message_id} not found")
return
# Check if there's already an argument or dialogue in progress
dialogue_manager = get_dialogue_manager()
if dialogue_manager.is_dialogue_active(message.channel.id):
print(f"⚠️ Dialogue already active in channel {message.channel.id}")
logger.error(f"Dialogue already active in channel {message.channel.id}")
return
if is_argument_in_progress(message.channel.id):
print(f"⚠️ Argument already in progress in channel {message.channel.id}")
logger.error(f"Argument already in progress in channel {message.channel.id}")
return
# Determine current persona from the message author
@@ -441,12 +510,12 @@ def trigger_dialogue(data: dict):
current_persona = "evil" if globals.EVIL_MODE else "miku"
else:
# User message - can't trigger dialogue from user messages
print(f"⚠️ Cannot trigger dialogue from user message")
logger.error(f"Cannot trigger dialogue from user message")
return
opposite_persona = "evil" if current_persona == "miku" else "miku"
print(f"🎭 [Manual Trigger] Forcing {opposite_persona} to start dialogue on message {message_id}")
logger.info(f"[Manual Trigger] Forcing {opposite_persona} to start dialogue on message {message_id}")
# Force start the dialogue (bypass interjection check)
dialogue_manager.start_dialogue(message.channel.id)
@@ -459,7 +528,7 @@ def trigger_dialogue(data: dict):
)
except Exception as e:
print(f"⚠️ Error triggering dialogue: {e}")
logger.error(f"Error triggering dialogue: {e}")
import traceback
traceback.print_exc()
@@ -514,8 +583,6 @@ def get_gpu_status():
@app.post("/gpu-select")
async def select_gpu(request: Request):
"""Select which GPU to use for inference"""
from utils.gpu_preload import preload_amd_models
data = await request.json()
gpu = data.get("gpu", "nvidia").lower()
@@ -532,16 +599,10 @@ async def select_gpu(request: Request):
with open(gpu_state_file, "w") as f:
json.dump(state, f, indent=2)
print(f"🎮 GPU Selection: Switched to {gpu.upper()} GPU")
# Preload models on AMD GPU (16GB VRAM - can hold both text + vision)
if gpu == "amd":
asyncio.create_task(preload_amd_models())
print("🔧 Preloading text and vision models on AMD GPU...")
logger.info(f"GPU Selection: Switched to {gpu.upper()} GPU")
return {"status": "ok", "message": f"Switched to {gpu.upper()} GPU", "gpu": gpu}
except Exception as e:
print(f"🎮 GPU Selection Error: {e}")
logger.error(f"GPU Selection Error: {e}")
return {"status": "error", "message": str(e)}
@app.get("/bipolar-mode/arguments")
@@ -574,17 +635,17 @@ async def set_server_mood_endpoint(guild_id: int, data: MoodSetRequest):
# Check if server exists
if guild_id not in server_manager.servers:
print(f"🎭 API: Server {guild_id} not found in server_manager.servers")
logger.warning(f"Server {guild_id} not found in server_manager.servers")
return {"status": "error", "message": "Server not found"}
# Check if mood is valid
from utils.moods import MOOD_EMOJIS
if data.mood not in MOOD_EMOJIS:
print(f"🎭 API: Mood '{data.mood}' not found in MOOD_EMOJIS. Available moods: {list(MOOD_EMOJIS.keys())}")
logger.warning(f"Mood '{data.mood}' not found in MOOD_EMOJIS. Available moods: {list(MOOD_EMOJIS.keys())}")
return {"status": "error", "message": f"Mood '{data.mood}' not recognized. Available moods: {', '.join(MOOD_EMOJIS.keys())}"}
success = server_manager.set_server_mood(guild_id, data.mood)
print(f"🎭 API: Server mood set result: {success}")
logger.debug(f"Server mood set result: {success}")
if success:
# V2: Notify autonomous engine of mood change
@@ -592,30 +653,30 @@ async def set_server_mood_endpoint(guild_id: int, data: MoodSetRequest):
from utils.autonomous import on_mood_change
on_mood_change(guild_id, data.mood)
except Exception as e:
print(f"⚠️ API: Failed to notify autonomous engine of mood change: {e}")
logger.error(f"Failed to notify autonomous engine of mood change: {e}")
# Update the nickname for this server
from utils.moods import update_server_nickname
print(f"🎭 API: Updating nickname for server {guild_id}")
logger.debug(f"Updating nickname for server {guild_id}")
globals.client.loop.create_task(update_server_nickname(guild_id))
return {"status": "ok", "new_mood": data.mood, "guild_id": guild_id}
print(f"🎭 API: set_server_mood returned False for unknown reason")
logger.warning(f"set_server_mood returned False for unknown reason")
return {"status": "error", "message": "Failed to set server mood"}
@app.post("/servers/{guild_id}/mood/reset")
async def reset_server_mood_endpoint(guild_id: int):
"""Reset mood to neutral for a specific server"""
print(f"🎭 API: Resetting mood for server {guild_id} to neutral")
logger.debug(f"Resetting mood for server {guild_id} to neutral")
# Check if server exists
if guild_id not in server_manager.servers:
print(f"🎭 API: Server {guild_id} not found in server_manager.servers")
logger.warning(f"Server {guild_id} not found in server_manager.servers")
return {"status": "error", "message": "Server not found"}
print(f"🎭 API: Server validation passed, calling set_server_mood")
logger.debug(f"Server validation passed, calling set_server_mood")
success = server_manager.set_server_mood(guild_id, "neutral")
print(f"🎭 API: Server mood reset result: {success}")
logger.debug(f"Server mood reset result: {success}")
if success:
# V2: Notify autonomous engine of mood change
@@ -623,15 +684,15 @@ async def reset_server_mood_endpoint(guild_id: int):
from utils.autonomous import on_mood_change
on_mood_change(guild_id, "neutral")
except Exception as e:
print(f"⚠️ API: Failed to notify autonomous engine of mood reset: {e}")
logger.error(f"Failed to notify autonomous engine of mood reset: {e}")
# Update the nickname for this server
from utils.moods import update_server_nickname
print(f"🎭 API: Updating nickname for server {guild_id}")
logger.debug(f"Updating nickname for server {guild_id}")
globals.client.loop.create_task(update_server_nickname(guild_id))
return {"status": "ok", "new_mood": "neutral", "guild_id": guild_id}
print(f"🎭 API: set_server_mood returned False for unknown reason")
logger.warning(f"set_server_mood returned False for unknown reason")
return {"status": "error", "message": "Failed to reset server mood"}
@app.get("/servers/{guild_id}/mood/state")
@@ -788,22 +849,22 @@ async def trigger_autonomous_reaction(guild_id: int = None):
async def trigger_detect_and_join_conversation(guild_id: int = None):
# If guild_id is provided, detect and join conversation only for that server
# If no guild_id, trigger for all servers
print(f"🔍 [API] Join conversation endpoint called with guild_id={guild_id}")
logger.debug(f"Join conversation endpoint called with guild_id={guild_id}")
if globals.client and globals.client.loop and globals.client.loop.is_running():
if guild_id is not None:
# Trigger for specific server only (force=True to bypass checks when manually triggered)
print(f"🔍 [API] Importing and calling miku_detect_and_join_conversation_for_server({guild_id}, force=True)")
logger.debug(f"Importing and calling miku_detect_and_join_conversation_for_server({guild_id}, force=True)")
from utils.autonomous import miku_detect_and_join_conversation_for_server
globals.client.loop.create_task(miku_detect_and_join_conversation_for_server(guild_id, force=True))
return {"status": "ok", "message": f"Detect and join conversation queued for server {guild_id}"}
else:
# Trigger for all servers (force=True to bypass checks when manually triggered)
print(f"🔍 [API] Importing and calling miku_detect_and_join_conversation() for all servers")
logger.debug(f"Importing and calling miku_detect_and_join_conversation() for all servers")
from utils.autonomous import miku_detect_and_join_conversation
globals.client.loop.create_task(miku_detect_and_join_conversation(force=True))
return {"status": "ok", "message": "Detect and join conversation queued for all servers"}
else:
print(f"⚠️ [API] Bot not ready: client={globals.client}, loop={globals.client.loop if globals.client else None}")
logger.error(f"Bot not ready: client={globals.client}, loop={globals.client.loop if globals.client else None}")
return {"status": "error", "message": "Bot not ready"}
@app.post("/profile-picture/change")
@@ -834,7 +895,7 @@ async def trigger_profile_picture_change(
custom_image_bytes = None
if file:
custom_image_bytes = await file.read()
print(f"🖼️ Received custom image upload ({len(custom_image_bytes)} bytes)")
logger.info(f"Received custom image upload ({len(custom_image_bytes)} bytes)")
# Change profile picture
result = await profile_picture_manager.change_profile_picture(
@@ -858,7 +919,7 @@ async def trigger_profile_picture_change(
}
except Exception as e:
print(f"⚠️ Error in profile picture API: {e}")
logger.error(f"Error in profile picture API: {e}")
import traceback
traceback.print_exc()
return {"status": "error", "message": f"Unexpected error: {str(e)}"}
@@ -955,7 +1016,7 @@ async def manual_send(
'content': file_content
})
except Exception as e:
print(f"Failed to read file {file.filename}: {e}")
logger.error(f"Failed to read file {file.filename}: {e}")
return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}
# Use create_task to avoid timeout context manager error
@@ -967,28 +1028,28 @@ async def manual_send(
try:
reference_message = await channel.fetch_message(int(reply_to_message_id))
except Exception as e:
print(f"⚠️ Could not fetch message {reply_to_message_id} for reply: {e}")
logger.error(f"Could not fetch message {reply_to_message_id} for reply: {e}")
return
# Send the main message
if message.strip():
if reference_message:
await channel.send(message, reference=reference_message, mention_author=mention_author)
print(f"Manual message sent as reply to #{channel.name}")
logger.info(f"Manual message sent as reply to #{channel.name}")
else:
await channel.send(message)
print(f"Manual message sent to #{channel.name}")
logger.info(f"Manual message sent to #{channel.name}")
# Send files if any
for file_info in file_data:
try:
await channel.send(file=discord.File(io.BytesIO(file_info['content']), filename=file_info['filename']))
print(f"File {file_info['filename']} sent to #{channel.name}")
logger.info(f"File {file_info['filename']} sent to #{channel.name}")
except Exception as e:
print(f"Failed to send file {file_info['filename']}: {e}")
logger.error(f"Failed to send file {file_info['filename']}: {e}")
except Exception as e:
print(f"Failed to send message: {e}")
logger.error(f"Failed to send message: {e}")
globals.client.loop.create_task(send_message_and_files())
return {"status": "ok", "message": "Message and files queued for sending"}
@@ -1028,7 +1089,7 @@ async def manual_send_webhook(
'content': file_content
})
except Exception as e:
print(f"Failed to read file {file.filename}: {e}")
logger.error(f"Failed to read file {file.filename}: {e}")
return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}
# Use create_task to avoid timeout context manager error
@@ -1037,7 +1098,7 @@ async def manual_send_webhook(
# Get or create webhooks for this channel (inside the task)
webhooks = await get_or_create_webhooks_for_channel(channel)
if not webhooks:
print(f"Failed to create webhooks for channel #{channel.name}")
logger.error(f"Failed to create webhooks for channel #{channel.name}")
return
# Select the appropriate webhook
@@ -1065,10 +1126,10 @@ async def manual_send_webhook(
)
persona_name = "Evil Miku" if persona == "evil" else "Hatsune Miku"
print(f"Manual webhook message sent as {persona_name} to #{channel.name}")
logger.info(f"Manual webhook message sent as {persona_name} to #{channel.name}")
except Exception as e:
print(f"Failed to send webhook message: {e}")
logger.error(f"Failed to send webhook message: {e}")
import traceback
traceback.print_exc()
@@ -1196,7 +1257,7 @@ async def delete_figurine_subscriber(user_id: str):
async def figurines_send_now(tweet_url: str = Form(None)):
"""Trigger immediate figurine DM send to all subscribers, optionally with specific tweet URL"""
if globals.client and globals.client.loop and globals.client.loop.is_running():
print(f"🚀 API: Sending figurine DMs to all subscribers, tweet_url: {tweet_url}")
logger.info(f"Sending figurine DMs to all subscribers, tweet_url: {tweet_url}")
globals.client.loop.create_task(send_figurine_dm_to_all_subscribers(globals.client, tweet_url=tweet_url))
return {"status": "ok", "message": "Figurine DMs queued"}
return {"status": "error", "message": "Bot not ready"}
@@ -1205,24 +1266,24 @@ async def figurines_send_now(tweet_url: str = Form(None)):
@app.post("/figurines/send_to_user")
async def figurines_send_to_user(user_id: str = Form(...), tweet_url: str = Form(None)):
"""Send figurine DM to a specific user, optionally with specific tweet URL"""
print(f"🎯 API: Received figurine send request - user_id: '{user_id}', tweet_url: '{tweet_url}'")
logger.debug(f"Received figurine send request - user_id: '{user_id}', tweet_url: '{tweet_url}'")
if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
print("❌ API: Bot not ready")
logger.error("Bot not ready")
return {"status": "error", "message": "Bot not ready"}
try:
user_id_int = int(user_id)
print(f"✅ API: Parsed user_id as {user_id_int}")
logger.debug(f"Parsed user_id as {user_id_int}")
except ValueError:
print(f"❌ API: Invalid user ID: '{user_id}'")
logger.error(f"Invalid user ID: '{user_id}'")
return {"status": "error", "message": "Invalid user ID"}
# Clean up tweet URL if it's empty string
if tweet_url == "":
tweet_url = None
print(f"🎯 API: Sending figurine DM to user {user_id_int}, tweet_url: {tweet_url}")
logger.info(f"Sending figurine DM to user {user_id_int}, tweet_url: {tweet_url}")
# Queue the DM send task in the bot's event loop
globals.client.loop.create_task(send_figurine_dm_to_single_user(globals.client, user_id_int, tweet_url=tweet_url))
@@ -1233,22 +1294,22 @@ async def figurines_send_to_user(user_id: str = Form(...), tweet_url: str = Form
@app.get("/servers")
def get_servers():
"""Get all configured servers"""
print(f"🎭 API: /servers endpoint called")
print(f"🎭 API: server_manager.servers keys: {list(server_manager.servers.keys())}")
print(f"🎭 API: server_manager.servers count: {len(server_manager.servers)}")
logger.debug("/servers endpoint called")
logger.debug(f"server_manager.servers keys: {list(server_manager.servers.keys())}")
logger.debug(f"server_manager.servers count: {len(server_manager.servers)}")
# Debug: Check config file directly
config_file = server_manager.config_file
print(f"🎭 API: Config file path: {config_file}")
logger.debug(f"Config file path: {config_file}")
if os.path.exists(config_file):
try:
with open(config_file, "r", encoding="utf-8") as f:
config_data = json.load(f)
print(f"🎭 API: Config file contains: {list(config_data.keys())}")
logger.debug(f"Config file contains: {list(config_data.keys())}")
except Exception as e:
print(f"🎭 API: Failed to read config file: {e}")
logger.error(f"Failed to read config file: {e}")
else:
print(f"🎭 API: Config file does not exist")
logger.warning("Config file does not exist")
servers = []
for server in server_manager.get_all_servers():
@@ -1260,10 +1321,10 @@ def get_servers():
server_data['guild_id'] = str(server_data['guild_id'])
servers.append(server_data)
print(f"🎭 API: Adding server to response: {server_data['guild_id']} - {server_data['guild_name']}")
print(f"🎭 API: Server data type check - guild_id: {type(server_data['guild_id'])}, value: {server_data['guild_id']}")
logger.debug(f"Adding server to response: {server_data['guild_id']} - {server_data['guild_name']}")
logger.debug(f"Server data type check - guild_id: {type(server_data['guild_id'])}, value: {server_data['guild_id']}")
print(f"🎭 API: Returning {len(servers)} servers")
logger.debug(f"Returning {len(servers)} servers")
# Debug: Show exact JSON being sent
import json
@@ -1316,7 +1377,7 @@ def update_server(guild_id: int, data: dict):
@app.post("/servers/{guild_id}/bedtime-range")
def update_server_bedtime_range(guild_id: int, data: dict):
"""Update server bedtime range configuration"""
print(f"⏰ API: Updating bedtime range for server {guild_id}: {data}")
logger.debug(f"Updating bedtime range for server {guild_id}: {data}")
# Validate the data
required_fields = ['bedtime_hour', 'bedtime_minute', 'bedtime_hour_end', 'bedtime_minute_end']
@@ -1346,7 +1407,7 @@ def update_server_bedtime_range(guild_id: int, data: dict):
# Update just the bedtime job for this server (avoid restarting all schedulers)
job_success = server_manager.update_server_bedtime_job(guild_id, globals.client)
if job_success:
print(f"✅ API: Bedtime range updated for server {guild_id}")
logger.info(f"Bedtime range updated for server {guild_id}")
return {
"status": "ok",
"message": f"Bedtime range updated: {bedtime_hour:02d}:{bedtime_minute:02d} - {bedtime_hour_end:02d}:{bedtime_minute_end:02d}"
@@ -1413,14 +1474,14 @@ async def send_custom_prompt_dm(user_id: str, req: CustomPromptRequest):
try:
response = await query_llama(req.prompt, user_id=user_id, guild_id=None, response_type="dm_response")
await user.send(response)
print(f"Custom DM prompt sent to user {user_id}: {req.prompt[:50]}...")
logger.info(f"Custom DM prompt sent to user {user_id}: {req.prompt[:50]}...")
# Log to DM history
from utils.dm_logger import dm_logger
dm_logger.log_conversation(user_id, req.prompt, response)
except Exception as e:
print(f"Failed to send custom DM prompt to user {user_id}: {e}")
logger.error(f"Failed to send custom DM prompt to user {user_id}: {e}")
# Use create_task to avoid timeout context manager error
globals.client.loop.create_task(send_dm_custom_prompt())
@@ -1456,7 +1517,7 @@ async def send_manual_message_dm(
'content': file_content
})
except Exception as e:
print(f"Failed to read file {file.filename}: {e}")
logger.error(f"Failed to read file {file.filename}: {e}")
return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}
async def send_dm_message_and_files():
@@ -1468,32 +1529,32 @@ async def send_manual_message_dm(
dm_channel = user.dm_channel or await user.create_dm()
reference_message = await dm_channel.fetch_message(int(reply_to_message_id))
except Exception as e:
print(f"⚠️ Could not fetch DM message {reply_to_message_id} for reply: {e}")
logger.error(f"Could not fetch DM message {reply_to_message_id} for reply: {e}")
return
# Send the main message
if message.strip():
if reference_message:
await user.send(message, reference=reference_message, mention_author=mention_author)
print(f"Manual DM reply message sent to user {user_id}")
logger.info(f"Manual DM reply message sent to user {user_id}")
else:
await user.send(message)
print(f"Manual DM message sent to user {user_id}")
logger.info(f"Manual DM message sent to user {user_id}")
# Send files if any
for file_info in file_data:
try:
await user.send(file=discord.File(io.BytesIO(file_info['content']), filename=file_info['filename']))
print(f"File {file_info['filename']} sent via DM to user {user_id}")
logger.info(f"File {file_info['filename']} sent via DM to user {user_id}")
except Exception as e:
print(f"Failed to send file {file_info['filename']} via DM: {e}")
logger.error(f"Failed to send file {file_info['filename']} via DM: {e}")
# Log to DM history (user message = manual override trigger, miku response = the message sent)
from utils.dm_logger import dm_logger
dm_logger.log_conversation(user_id, "[Manual Override Trigger]", message, attachments=[f['filename'] for f in file_data])
except Exception as e:
print(f"Failed to send manual DM to user {user_id}: {e}")
logger.error(f"Failed to send manual DM to user {user_id}: {e}")
# Use create_task to avoid timeout context manager error
globals.client.loop.create_task(send_dm_message_and_files())
@@ -1559,7 +1620,7 @@ async def test_image_detection(req: dict):
async def view_generated_image(filename: str):
"""Serve generated images from ComfyUI output directory"""
try:
print(f"🖼️ Image view request for: {filename}")
logger.debug(f"Image view request for: {filename}")
# Try multiple possible paths for ComfyUI output
possible_paths = [
@@ -1572,13 +1633,13 @@ async def view_generated_image(filename: str):
for path in possible_paths:
if os.path.exists(path):
image_path = path
print(f"Found image at: {path}")
logger.debug(f"Found image at: {path}")
break
else:
print(f"Not found at: {path}")
logger.debug(f"Not found at: {path}")
if not image_path:
print(f"Image not found anywhere: {filename}")
logger.warning(f"Image not found anywhere: {filename}")
return {"status": "error", "message": f"Image not found: {filename}"}
# Determine content type based on file extension
@@ -1591,11 +1652,11 @@ async def view_generated_image(filename: str):
elif ext == "webp":
content_type = "image/webp"
print(f"📤 Serving image: {image_path} as {content_type}")
logger.info(f"Serving image: {image_path} as {content_type}")
return FileResponse(image_path, media_type=content_type)
except Exception as e:
print(f"Error serving image: {e}")
logger.error(f"Error serving image: {e}")
return {"status": "error", "message": f"Error serving image: {e}"}
@app.post("/servers/{guild_id}/autonomous/tweet")
@@ -1638,36 +1699,36 @@ def get_available_moods():
@app.post("/test/mood/{guild_id}")
async def test_mood_change(guild_id: int, data: MoodSetRequest):
"""Test endpoint for debugging mood changes"""
print(f"🧪 TEST: Testing mood change for server {guild_id} to {data.mood}")
logger.debug(f"TEST: Testing mood change for server {guild_id} to {data.mood}")
# Check if server exists
if guild_id not in server_manager.servers:
return {"status": "error", "message": f"Server {guild_id} not found"}
server_config = server_manager.get_server_config(guild_id)
print(f"🧪 TEST: Server config found: {server_config.guild_name if server_config else 'None'}")
logger.debug(f"TEST: Server config found: {server_config.guild_name if server_config else 'None'}")
# Try to set mood
success = server_manager.set_server_mood(guild_id, data.mood)
print(f"🧪 TEST: Mood set result: {success}")
logger.debug(f"TEST: Mood set result: {success}")
if success:
# V2: Notify autonomous engine of mood change
try:
from utils.autonomous import on_mood_change
on_mood_change(guild_id, data.mood)
print(f"🧪 TEST: Notified autonomous engine of mood change")
logger.debug(f"TEST: Notified autonomous engine of mood change")
except Exception as e:
print(f"⚠️ TEST: Failed to notify autonomous engine: {e}")
logger.error(f"TEST: Failed to notify autonomous engine: {e}")
# Try to update nickname
from utils.moods import update_server_nickname
print(f"🧪 TEST: Attempting nickname update...")
logger.debug(f"TEST: Attempting nickname update...")
try:
await update_server_nickname(guild_id)
print(f"🧪 TEST: Nickname update completed")
logger.debug(f"TEST: Nickname update completed")
except Exception as e:
print(f"🧪 TEST: Nickname update failed: {e}")
logger.error(f"TEST: Nickname update failed: {e}")
import traceback
traceback.print_exc()
@@ -1707,10 +1768,10 @@ def get_dm_conversations(user_id: str, limit: int = 50):
from utils.dm_logger import dm_logger
# Convert string user_id to int for internal processing
user_id_int = int(user_id)
print(f"🔍 API: Loading conversations for user {user_id_int}, limit: {limit}")
logger.debug(f"Loading conversations for user {user_id_int}, limit: {limit}")
logs = dm_logger._load_user_logs(user_id_int)
print(f"🔍 API: Loaded logs for user {user_id_int}: {len(logs.get('conversations', []))} conversations")
logger.debug(f"Loaded logs for user {user_id_int}: {len(logs.get('conversations', []))} conversations")
conversations = logs["conversations"][-limit:] if limit > 0 else logs["conversations"]
@@ -1719,20 +1780,20 @@ def get_dm_conversations(user_id: str, limit: int = 50):
if "message_id" in conv:
conv["message_id"] = str(conv["message_id"])
print(f"🔍 API: Returning {len(conversations)} conversations")
logger.debug(f"Returning {len(conversations)} conversations")
# Debug: Show message IDs being returned
for i, conv in enumerate(conversations):
msg_id = conv.get("message_id", "")
is_bot = conv.get("is_bot_message", False)
content_preview = conv.get("content", "")[:30] + "..." if conv.get("content", "") else "[No content]"
print(f"🔍 API: Conv {i}: id={msg_id} (type: {type(msg_id)}), is_bot={is_bot}, content='{content_preview}'")
logger.debug(f"Conv {i}: id={msg_id} (type: {type(msg_id)}), is_bot={is_bot}, content='{content_preview}'")
return {"status": "ok", "conversations": conversations}
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
print(f"❌ API: Failed to get conversations for user {user_id}: {e}")
logger.error(f"Failed to get conversations for user {user_id}: {e}")
return {"status": "error", "message": f"Failed to get conversations: {e}"}
@app.get("/dms/users/{user_id}/search")
@@ -1792,7 +1853,7 @@ def get_blocked_users():
blocked_users = dm_logger.get_blocked_users()
return {"status": "ok", "blocked_users": blocked_users}
except Exception as e:
print(f"❌ API: Failed to get blocked users: {e}")
logger.error(f"Failed to get blocked users: {e}")
return {"status": "error", "message": f"Failed to get blocked users: {e}"}
@app.post("/dms/users/{user_id}/block")
@@ -1808,7 +1869,7 @@ def block_user(user_id: str):
success = dm_logger.block_user(user_id_int, username)
if success:
print(f"🚫 API: User {user_id} ({username}) blocked")
logger.info(f"User {user_id} ({username}) blocked")
return {"status": "ok", "message": f"User {username} has been blocked"}
else:
return {"status": "error", "message": f"User {username} is already blocked"}
@@ -1816,7 +1877,7 @@ def block_user(user_id: str):
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
print(f"❌ API: Failed to block user {user_id}: {e}")
logger.error(f"Failed to block user {user_id}: {e}")
return {"status": "error", "message": f"Failed to block user: {e}"}
@app.post("/dms/users/{user_id}/unblock")
@@ -1827,7 +1888,7 @@ def unblock_user(user_id: str):
success = dm_logger.unblock_user(user_id_int)
if success:
print(f"✅ API: User {user_id} unblocked")
logger.info(f"User {user_id} unblocked")
return {"status": "ok", "message": f"User has been unblocked"}
else:
return {"status": "error", "message": f"User is not blocked"}
@@ -1835,7 +1896,7 @@ def unblock_user(user_id: str):
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
print(f"❌ API: Failed to unblock user {user_id}: {e}")
logger.error(f"Failed to unblock user {user_id}: {e}")
return {"status": "error", "message": f"Failed to unblock user: {e}"}
@app.post("/dms/users/{user_id}/conversations/{conversation_id}/delete")
@@ -1853,13 +1914,13 @@ def delete_conversation(user_id: str, conversation_id: str):
# For now, return success immediately since we can't await in FastAPI sync endpoint
# The actual deletion happens asynchronously
print(f"🗑️ API: Queued deletion of conversation {conversation_id} for user {user_id}")
logger.info(f"Queued deletion of conversation {conversation_id} for user {user_id}")
return {"status": "ok", "message": "Message deletion queued (will delete from both Discord and logs)"}
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
print(f"❌ API: Failed to queue conversation deletion {conversation_id}: {e}")
logger.error(f"Failed to queue conversation deletion {conversation_id}: {e}")
return {"status": "error", "message": f"Failed to delete conversation: {e}"}
@app.post("/dms/users/{user_id}/conversations/delete-all")
@@ -1876,13 +1937,13 @@ def delete_all_conversations(user_id: str):
success = globals.client.loop.create_task(do_delete_all())
# Return success immediately since we can't await in FastAPI sync endpoint
print(f"🗑️ API: Queued bulk deletion of all conversations for user {user_id}")
logger.info(f"Queued bulk deletion of all conversations for user {user_id}")
return {"status": "ok", "message": "Bulk deletion queued (will delete all Miku messages from Discord and clear logs)"}
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
print(f"❌ API: Failed to queue bulk conversation deletion for user {user_id}: {e}")
logger.error(f"Failed to queue bulk conversation deletion for user {user_id}: {e}")
return {"status": "error", "message": f"Failed to delete conversations: {e}"}
@app.post("/dms/users/{user_id}/delete-completely")
@@ -1893,7 +1954,7 @@ def delete_user_completely(user_id: str):
success = dm_logger.delete_user_completely(user_id_int)
if success:
print(f"🗑️ API: Completely deleted user {user_id}")
logger.info(f"Completely deleted user {user_id}")
return {"status": "ok", "message": "User data deleted completely"}
else:
return {"status": "error", "message": "No user data found"}
@@ -1901,7 +1962,7 @@ def delete_user_completely(user_id: str):
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
print(f"❌ API: Failed to completely delete user {user_id}: {e}")
logger.error(f"Failed to completely delete user {user_id}: {e}")
return {"status": "error", "message": f"Failed to delete user: {e}"}
# ========== DM Interaction Analysis Endpoints ==========
@@ -1923,7 +1984,7 @@ def run_dm_analysis():
return {"status": "ok", "message": "DM analysis started"}
except Exception as e:
print(f"❌ API: Failed to run DM analysis: {e}")
logger.error(f"Failed to run DM analysis: {e}")
return {"status": "error", "message": f"Failed to run DM analysis: {e}"}
@app.post("/dms/users/{user_id}/analyze")
@@ -1949,7 +2010,7 @@ def analyze_user_interaction(user_id: str):
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
print(f"❌ API: Failed to analyze user {user_id}: {e}")
logger.error(f"Failed to analyze user {user_id}: {e}")
return {"status": "error", "message": f"Failed to analyze user: {e}"}
@app.get("/dms/analysis/reports")
@@ -1974,11 +2035,11 @@ def get_analysis_reports(limit: int = 20):
report['filename'] = filename
reports.append(report)
except Exception as e:
print(f"⚠️ Failed to load report {filename}: {e}")
logger.warning(f"Failed to load report {filename}: {e}")
return {"status": "ok", "reports": reports}
except Exception as e:
print(f"❌ API: Failed to get reports: {e}")
logger.error(f"Failed to get reports: {e}")
return {"status": "error", "message": f"Failed to get reports: {e}"}
@app.get("/dms/analysis/reports/{user_id}")
@@ -2005,13 +2066,13 @@ def get_user_reports(user_id: str, limit: int = 10):
report['filename'] = filename
reports.append(report)
except Exception as e:
print(f"⚠️ Failed to load report {filename}: {e}")
logger.warning(f"Failed to load report {filename}: {e}")
return {"status": "ok", "reports": reports}
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
print(f"❌ API: Failed to get user reports: {e}")
logger.error(f"Failed to get user reports: {e}")
return {"status": "error", "message": f"Failed to get user reports: {e}"}
# ========== Message Reaction Endpoint ==========
@@ -2043,15 +2104,15 @@ async def add_reaction_to_message(
try:
message = await channel.fetch_message(msg_id)
await message.add_reaction(emoji)
print(f"Added reaction {emoji} to message {msg_id} in channel #{channel.name}")
logger.info(f"Added reaction {emoji} to message {msg_id} in channel #{channel.name}")
except discord.NotFound:
print(f"Message {msg_id} not found in channel #{channel.name}")
logger.error(f"Message {msg_id} not found in channel #{channel.name}")
except discord.Forbidden:
print(f"Bot doesn't have permission to add reactions in channel #{channel.name}")
logger.error(f"Bot doesn't have permission to add reactions in channel #{channel.name}")
except discord.HTTPException as e:
print(f"Failed to add reaction: {e}")
logger.error(f"Failed to add reaction: {e}")
except Exception as e:
print(f"Unexpected error adding reaction: {e}")
logger.error(f"Unexpected error adding reaction: {e}")
globals.client.loop.create_task(add_reaction_task())
@@ -2061,7 +2122,7 @@ async def add_reaction_to_message(
}
except Exception as e:
print(f"❌ API: Failed to add reaction: {e}")
logger.error(f"Failed to add reaction: {e}")
return {"status": "error", "message": f"Failed to add reaction: {e}"}
# ========== Autonomous V2 Endpoints ==========
@@ -2290,7 +2351,7 @@ Be detailed but conversational. React to what you see with Miku's cheerful, play
except Exception as e:
error_msg = f"Error in chat stream: {str(e)}"
print(f"{error_msg}")
logger.error(error_msg)
yield f"data: {json.dumps({'error': error_msg})}\n\n"
return StreamingResponse(
@@ -2303,6 +2364,168 @@ Be detailed but conversational. React to what you see with Miku's cheerful, play
}
)
# ========== Log Management API ==========
class LogConfigUpdateRequest(BaseModel):
component: Optional[str] = None
enabled: Optional[bool] = None
enabled_levels: Optional[List[str]] = None
class LogFilterUpdateRequest(BaseModel):
exclude_paths: Optional[List[str]] = None
exclude_status: Optional[List[int]] = None
include_slow_requests: Optional[bool] = None
slow_threshold_ms: Optional[int] = None
@app.get("/api/log/config")
async def get_log_config():
"""Get current logging configuration."""
try:
config = load_log_config()
logger.debug("Log config requested")
return {"success": True, "config": config}
except Exception as e:
logger.error(f"Failed to get log config: {e}")
return {"success": False, "error": str(e)}
@app.post("/api/log/config")
async def update_log_config(request: LogConfigUpdateRequest):
"""Update logging configuration."""
try:
if request.component:
success = update_component(
request.component,
enabled=request.enabled,
enabled_levels=request.enabled_levels
)
if not success:
return {"success": False, "error": f"Failed to update component {request.component}"}
logger.info(f"Log config updated: component={request.component}, enabled_levels={request.enabled_levels}, enabled={request.enabled}")
return {"success": True, "message": "Configuration updated"}
except Exception as e:
logger.error(f"Failed to update log config: {e}")
return {"success": False, "error": str(e)}
@app.get("/api/log/components")
async def get_log_components():
"""Get list of all logging components with their descriptions."""
try:
components = list_components()
stats = get_component_stats()
logger.debug("Log components list requested")
return {
"success": True,
"components": components,
"stats": stats
}
except Exception as e:
logger.error(f"Failed to get log components: {e}")
return {"success": False, "error": str(e)}
@app.post("/api/log/reload")
async def reload_log_config():
"""Reload logging configuration from file."""
try:
success = reload_all_loggers()
if success:
logger.info("Log configuration reloaded")
return {"success": True, "message": "Configuration reloaded"}
else:
return {"success": False, "error": "Failed to reload configuration"}
except Exception as e:
logger.error(f"Failed to reload log config: {e}")
return {"success": False, "error": str(e)}
@app.post("/api/log/filters")
async def update_log_filters(request: LogFilterUpdateRequest):
"""Update API request filtering configuration."""
try:
success = update_api_filters(
exclude_paths=request.exclude_paths,
exclude_status=request.exclude_status,
include_slow_requests=request.include_slow_requests,
slow_threshold_ms=request.slow_threshold_ms
)
if success:
logger.info(f"API filters updated: {request.dict(exclude_none=True)}")
return {"success": True, "message": "Filters updated"}
else:
return {"success": False, "error": "Failed to update filters"}
except Exception as e:
logger.error(f"Failed to update filters: {e}")
return {"success": False, "error": str(e)}
@app.post("/api/log/reset")
async def reset_log_config():
"""Reset logging configuration to defaults."""
try:
success = reset_to_defaults()
if success:
logger.info("Log configuration reset to defaults")
return {"success": True, "message": "Configuration reset to defaults"}
else:
return {"success": False, "error": "Failed to reset configuration"}
except Exception as e:
logger.error(f"Failed to reset log config: {e}")
return {"success": False, "error": str(e)}
@app.post("/api/log/global-level")
async def update_global_level_endpoint(level: str, enabled: bool):
"""Enable or disable a specific log level across all components."""
try:
from utils.log_config import update_global_level
success = update_global_level(level, enabled)
if success:
action = "enabled" if enabled else "disabled"
logger.info(f"Global level {level} {action} across all components")
return {"success": True, "message": f"Level {level} {action} globally"}
else:
return {"success": False, "error": f"Failed to update global level {level}"}
except Exception as e:
logger.error(f"Failed to update global level: {e}")
return {"success": False, "error": str(e)}
@app.post("/api/log/timestamp-format")
async def update_timestamp_format_endpoint(format_type: str):
"""Update timestamp format for all log outputs."""
try:
success = update_timestamp_format(format_type)
if success:
logger.info(f"Timestamp format updated to: {format_type}")
return {"success": True, "message": f"Timestamp format set to: {format_type}"}
else:
return {"success": False, "error": f"Invalid timestamp format: {format_type}"}
except Exception as e:
logger.error(f"Failed to update timestamp format: {e}")
return {"success": False, "error": str(e)}
@app.get("/api/log/files/{component}")
async def get_log_file(component: str, lines: int = 100):
"""Get last N lines from a component's log file."""
try:
from pathlib import Path
log_dir = Path('/app/memory/logs')
log_file = log_dir / f'{component.replace(".", "_")}.log'
if not log_file.exists():
return {"success": False, "error": "Log file not found"}
with open(log_file, 'r', encoding='utf-8') as f:
all_lines = f.readlines()
last_lines = all_lines[-lines:] if len(all_lines) >= lines else all_lines
logger.debug(f"Log file requested: {component} ({lines} lines)")
return {
"success": True,
"component": component,
"lines": last_lines,
"total_lines": len(all_lines)
}
except Exception as e:
logger.error(f"Failed to read log file for {component}: {e}")
return {"success": False, "error": str(e)}
def start_api():
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=3939)

View File

@@ -46,9 +46,13 @@ from utils.autonomous import (
)
from utils.dm_logger import dm_logger
from utils.dm_interaction_analyzer import init_dm_analyzer
from utils.logger import get_logger
import globals
# Initialize bot logger
logger = get_logger('bot')
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s: %(message)s",
@@ -61,11 +65,15 @@ logging.basicConfig(
@globals.client.event
async def on_ready():
print(f'🎤 MikuBot connected as {globals.client.user}')
print(f'💬 DM support enabled - users can message Miku directly!')
logger.info(f'🎤 MikuBot connected as {globals.client.user}')
logger.info(f'💬 DM support enabled - users can message Miku directly!')
globals.BOT_USER = globals.client.user
# Intercept external library loggers (APScheduler, etc.)
from utils.logger import intercept_external_loggers
intercept_external_loggers()
# Restore evil mode state from previous session (if any)
from utils.evil_mode import restore_evil_mode_on_startup
restore_evil_mode_on_startup()
@@ -77,7 +85,7 @@ async def on_ready():
# Initialize DM interaction analyzer
if globals.OWNER_USER_ID and globals.OWNER_USER_ID != 0:
init_dm_analyzer(globals.OWNER_USER_ID)
print(f"📊 DM Interaction Analyzer initialized for owner ID: {globals.OWNER_USER_ID}")
logger.info(f"📊 DM Interaction Analyzer initialized for owner ID: {globals.OWNER_USER_ID}")
# Schedule daily DM analysis (runs at 2 AM every day)
from utils.scheduled import run_daily_dm_analysis
@@ -88,9 +96,9 @@ async def on_ready():
minute=0,
id='daily_dm_analysis'
)
print("⏰ Scheduled daily DM analysis at 2:00 AM")
logger.info("⏰ Scheduled daily DM analysis at 2:00 AM")
else:
print("⚠️ OWNER_USER_ID not set, DM analysis feature disabled")
logger.warning("OWNER_USER_ID not set, DM analysis feature disabled")
# Setup autonomous speaking (now handled by server manager)
setup_autonomous_speaking()
@@ -146,7 +154,7 @@ async def on_message(message):
await replied_msg.reply(file=discord.File(output_video))
except Exception as e:
print(f"⚠️ Error processing video: {e}")
logger.error(f"Error processing video: {e}")
await message.channel.send("Sorry, something went wrong while generating the video.")
return
@@ -159,11 +167,11 @@ async def on_message(message):
miku_addressed = await is_miku_addressed(message)
if is_dm:
print(f"💌 DM from {message.author.display_name}: {message.content[:50]}{'...' if len(message.content) > 50 else ''}")
logger.info(f"💌 DM from {message.author.display_name}: {message.content[:50]}{'...' if len(message.content) > 50 else ''}")
# Check if user is blocked
if dm_logger.is_user_blocked(message.author.id):
print(f"🚫 Blocked user {message.author.display_name} ({message.author.id}) tried to send DM - ignoring")
logger.info(f"🚫 Blocked user {message.author.display_name} ({message.author.id}) tried to send DM - ignoring")
return
# Log the user's DM message
@@ -185,7 +193,7 @@ async def on_message(message):
# Add reply context marker to the prompt
prompt = f'[Replying to your message: "{replied_content}"] {prompt}'
except Exception as e:
print(f"⚠️ Failed to fetch replied message for context: {e}")
logger.error(f"Failed to fetch replied message for context: {e}")
async with message.channel.typing():
# If message has an image, video, or GIF attachment
@@ -212,9 +220,9 @@ async def on_message(message):
)
if is_dm:
print(f"💌 DM image response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
logger.info(f"💌 DM image response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
else:
print(f"💬 Server image response to {message.author.display_name} in {message.guild.name} (using server mood)")
logger.info(f"💬 Server image response to {message.author.display_name} in {message.guild.name} (using server mood)")
response_message = await message.channel.send(miku_reply)
@@ -229,7 +237,7 @@ async def on_message(message):
current_persona = "evil" if globals.EVIL_MODE else "miku"
asyncio.create_task(check_for_interjection(response_message, current_persona))
except Exception as e:
print(f"⚠️ Error checking for persona interjection: {e}")
logger.error(f"Error checking for persona interjection: {e}")
return
@@ -239,7 +247,7 @@ async def on_message(message):
is_gif = attachment.filename.lower().endswith('.gif')
media_type = "gif" if is_gif else "video"
print(f"🎬 Processing {media_type}: {attachment.filename}")
logger.debug(f"🎬 Processing {media_type}: {attachment.filename}")
# Download the media
media_bytes_b64 = await download_and_encode_media(attachment.url)
@@ -253,13 +261,13 @@ async def on_message(message):
# If it's a GIF, convert to MP4 for better processing
if is_gif:
print(f"🔄 Converting GIF to MP4 for processing...")
logger.debug(f"🔄 Converting GIF to MP4 for processing...")
mp4_bytes = await convert_gif_to_mp4(media_bytes)
if mp4_bytes:
media_bytes = mp4_bytes
print(f"✅ GIF converted to MP4")
logger.info(f"✅ GIF converted to MP4")
else:
print(f"⚠️ GIF conversion failed, trying direct processing")
logger.warning(f"GIF conversion failed, trying direct processing")
# Extract frames
frames = await extract_video_frames(media_bytes, num_frames=6)
@@ -268,7 +276,7 @@ async def on_message(message):
await message.channel.send(f"I couldn't extract frames from that {media_type}, sorry!")
return
print(f"📹 Extracted {len(frames)} frames from {attachment.filename}")
logger.debug(f"📹 Extracted {len(frames)} frames from {attachment.filename}")
# Analyze the video/GIF with appropriate media type
video_description = await analyze_video_with_vision(frames, media_type=media_type)
@@ -284,9 +292,9 @@ async def on_message(message):
)
if is_dm:
print(f"💌 DM {media_type} response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
logger.info(f"💌 DM {media_type} response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
else:
print(f"💬 Server video response to {message.author.display_name} in {message.guild.name} (using server mood)")
logger.info(f"💬 Server video response to {message.author.display_name} in {message.guild.name} (using server mood)")
response_message = await message.channel.send(miku_reply)
@@ -301,7 +309,7 @@ async def on_message(message):
current_persona = "evil" if globals.EVIL_MODE else "miku"
asyncio.create_task(check_for_interjection(response_message, current_persona))
except Exception as e:
print(f"⚠️ Error checking for persona interjection: {e}")
logger.error(f"Error checking for persona interjection: {e}")
return
@@ -310,7 +318,7 @@ async def on_message(message):
for embed in message.embeds:
# Handle Tenor GIF embeds specially (Discord uses these for /gif command)
if embed.type == 'gifv' and embed.url and 'tenor.com' in embed.url:
print(f"🎭 Processing Tenor GIF from embed: {embed.url}")
logger.info(f"🎭 Processing Tenor GIF from embed: {embed.url}")
# Extract the actual GIF URL from Tenor
gif_url = await extract_tenor_gif_url(embed.url)
@@ -322,7 +330,7 @@ async def on_message(message):
gif_url = embed.thumbnail.url
if not gif_url:
print(f"⚠️ Could not extract GIF URL from Tenor embed")
logger.warning(f"Could not extract GIF URL from Tenor embed")
continue
# Download the GIF
@@ -336,13 +344,13 @@ async def on_message(message):
media_bytes = base64.b64decode(media_bytes_b64)
# Convert GIF to MP4
print(f"🔄 Converting Tenor GIF to MP4 for processing...")
logger.debug(f"Converting Tenor GIF to MP4 for processing...")
mp4_bytes = await convert_gif_to_mp4(media_bytes)
if not mp4_bytes:
print(f"⚠️ GIF conversion failed, trying direct frame extraction")
logger.warning(f"GIF conversion failed, trying direct frame extraction")
mp4_bytes = media_bytes
else:
print(f"Tenor GIF converted to MP4")
logger.debug(f"Tenor GIF converted to MP4")
# Extract frames
frames = await extract_video_frames(mp4_bytes, num_frames=6)
@@ -351,7 +359,7 @@ async def on_message(message):
await message.channel.send("I couldn't extract frames from that GIF, sorry!")
return
print(f"📹 Extracted {len(frames)} frames from Tenor GIF")
logger.info(f"📹 Extracted {len(frames)} frames from Tenor GIF")
# Analyze the GIF with tenor_gif media type
video_description = await analyze_video_with_vision(frames, media_type="tenor_gif")
@@ -366,9 +374,9 @@ async def on_message(message):
)
if is_dm:
print(f"💌 DM Tenor GIF response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
logger.info(f"💌 DM Tenor GIF response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
else:
print(f"💬 Server Tenor GIF response to {message.author.display_name} in {message.guild.name} (using server mood)")
logger.info(f"💬 Server Tenor GIF response to {message.author.display_name} in {message.guild.name} (using server mood)")
response_message = await message.channel.send(miku_reply)
@@ -383,19 +391,19 @@ async def on_message(message):
current_persona = "evil" if globals.EVIL_MODE else "miku"
asyncio.create_task(check_for_interjection(response_message, current_persona))
except Exception as e:
print(f"⚠️ Error checking for persona interjection: {e}")
logger.error(f"Error checking for persona interjection: {e}")
return
# Handle other types of embeds (rich, article, image, video, link)
elif embed.type in ['rich', 'article', 'image', 'video', 'link']:
print(f"📰 Processing {embed.type} embed")
logger.error(f"Processing {embed.type} embed")
# Extract content from embed
embed_content = await extract_embed_content(embed)
if not embed_content['has_content']:
print(f"⚠️ Embed has no extractable content, skipping")
logger.warning(f"Embed has no extractable content, skipping")
continue
# Build context string with embed text
@@ -406,28 +414,28 @@ async def on_message(message):
# Process images from embed
if embed_content['images']:
for img_url in embed_content['images']:
print(f"🖼️ Processing image from embed: {img_url}")
logger.error(f"Processing image from embed: {img_url}")
try:
base64_img = await download_and_encode_image(img_url)
if base64_img:
print(f"Image downloaded, analyzing with vision model...")
logger.info(f"Image downloaded, analyzing with vision model...")
# Analyze image
qwen_description = await analyze_image_with_qwen(base64_img)
truncated = (qwen_description[:50] + "...") if len(qwen_description) > 50 else qwen_description
print(f"📝 Vision analysis result: {truncated}")
logger.error(f"Vision analysis result: {truncated}")
if qwen_description and qwen_description.strip():
embed_context_parts.append(f"[Embedded image shows: {qwen_description}]")
else:
print(f"Failed to download image from embed")
logger.error(f"Failed to download image from embed")
except Exception as e:
print(f"⚠️ Error processing embedded image: {e}")
logger.error(f"Error processing embedded image: {e}")
import traceback
traceback.print_exc()
# Process videos from embed
if embed_content['videos']:
for video_url in embed_content['videos']:
print(f"🎬 Processing video from embed: {video_url}")
logger.info(f"🎬 Processing video from embed: {video_url}")
try:
media_bytes_b64 = await download_and_encode_media(video_url)
if media_bytes_b64:
@@ -435,17 +443,17 @@ async def on_message(message):
media_bytes = base64.b64decode(media_bytes_b64)
frames = await extract_video_frames(media_bytes, num_frames=6)
if frames:
print(f"📹 Extracted {len(frames)} frames, analyzing with vision model...")
logger.info(f"📹 Extracted {len(frames)} frames, analyzing with vision model...")
video_description = await analyze_video_with_vision(frames, media_type="video")
print(f"📝 Video analysis result: {video_description[:100]}...")
logger.info(f"Video analysis result: {video_description[:100]}...")
if video_description and video_description.strip():
embed_context_parts.append(f"[Embedded video shows: {video_description}]")
else:
print(f"Failed to extract frames from video")
logger.error(f"Failed to extract frames from video")
else:
print(f"Failed to download video from embed")
logger.error(f"Failed to download video from embed")
except Exception as e:
print(f"⚠️ Error processing embedded video: {e}")
logger.error(f"Error processing embedded video: {e}")
import traceback
traceback.print_exc()
@@ -468,9 +476,9 @@ async def on_message(message):
)
if is_dm:
print(f"💌 DM embed response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
logger.info(f"💌 DM embed response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
else:
print(f"💬 Server embed response to {message.author.display_name} in {message.guild.name}")
logger.info(f"💬 Server embed response to {message.author.display_name} in {message.guild.name}")
response_message = await message.channel.send(response)
@@ -485,7 +493,7 @@ async def on_message(message):
current_persona = "evil" if globals.EVIL_MODE else "miku"
asyncio.create_task(check_for_interjection(response_message, current_persona))
except Exception as e:
print(f"⚠️ Error checking for persona interjection: {e}")
logger.error(f"Error checking for persona interjection: {e}")
return
@@ -494,7 +502,7 @@ async def on_message(message):
is_image_request, image_prompt = await detect_image_request(prompt)
if is_image_request and image_prompt:
print(f"🎨 Image generation request detected: '{image_prompt}' from {message.author.display_name}")
logger.info(f"🎨 Image generation request detected: '{image_prompt}' from {message.author.display_name}")
# Handle the image generation workflow
success = await handle_image_generation_request(message, image_prompt)
@@ -502,7 +510,7 @@ async def on_message(message):
return # Image generation completed successfully
# If image generation failed, fall back to normal response
print(f"⚠️ Image generation failed, falling back to normal response")
logger.warning(f"Image generation failed, falling back to normal response")
# If message is just a prompt, no image
# For DMs, pass None as guild_id to use DM mood
@@ -518,9 +526,9 @@ async def on_message(message):
)
if is_dm:
print(f"💌 DM response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
logger.info(f"💌 DM response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
else:
print(f"💬 Server response to {message.author.display_name} in {message.guild.name} (using server mood)")
logger.info(f"💬 Server response to {message.author.display_name} in {message.guild.name} (using server mood)")
response_message = await message.channel.send(response)
@@ -530,15 +538,15 @@ async def on_message(message):
# For server messages, check if opposite persona should interject (persona dialogue system)
if not is_dm and globals.BIPOLAR_MODE:
print(f"🔧 [DEBUG] Attempting to check for interjection (is_dm={is_dm}, BIPOLAR_MODE={globals.BIPOLAR_MODE})")
logger.debug(f"Attempting to check for interjection (is_dm={is_dm}, BIPOLAR_MODE={globals.BIPOLAR_MODE})")
try:
from utils.persona_dialogue import check_for_interjection
current_persona = "evil" if globals.EVIL_MODE else "miku"
print(f"🔧 [DEBUG] Creating interjection check task for persona: {current_persona}")
logger.debug(f"Creating interjection check task for persona: {current_persona}")
# Pass the bot's response message for analysis
asyncio.create_task(check_for_interjection(response_message, current_persona))
except Exception as e:
print(f"⚠️ Error checking for persona interjection: {e}")
logger.error(f"Error checking for persona interjection: {e}")
import traceback
traceback.print_exc()
@@ -557,11 +565,11 @@ async def on_message(message):
detected = detect_mood_shift(response, server_context)
if detected and detected != server_config.current_mood_name:
print(f"🔄 Auto mood detection for server {message.guild.name}: {server_config.current_mood_name} -> {detected}")
logger.info(f"🔄 Auto mood detection for server {message.guild.name}: {server_config.current_mood_name} -> {detected}")
# Block direct transitions to asleep unless from sleepy
if detected == "asleep" and server_config.current_mood_name != "sleepy":
print("Ignoring asleep mood; server wasn't sleepy before.")
logger.warning("Ignoring asleep mood; server wasn't sleepy before.")
else:
# Update server mood
server_manager.set_server_mood(message.guild.id, detected)
@@ -570,7 +578,7 @@ async def on_message(message):
from utils.moods import update_server_nickname
globals.client.loop.create_task(update_server_nickname(message.guild.id))
print(f"🔄 Server mood auto-updated to: {detected}")
logger.info(f"🔄 Server mood auto-updated to: {detected}")
if detected == "asleep":
server_manager.set_server_sleep_state(message.guild.id, True)
@@ -580,15 +588,15 @@ async def on_message(message):
server_manager.set_server_sleep_state(message.guild.id, False)
server_manager.set_server_mood(message.guild.id, "neutral")
await update_server_nickname(message.guild.id)
print(f"🌅 Server {message.guild.name} woke up from auto-sleep")
logger.info(f"🌅 Server {message.guild.name} woke up from auto-sleep")
globals.client.loop.create_task(delayed_wakeup())
else:
print(f"⚠️ No server config found for guild {message.guild.id}, skipping mood detection")
logger.error(f"No server config found for guild {message.guild.id}, skipping mood detection")
except Exception as e:
print(f"⚠️ Error in server mood detection: {e}")
logger.error(f"Error in server mood detection: {e}")
elif is_dm:
print("💌 DM message - no mood detection (DM mood only changes via auto-rotation)")
logger.debug("DM message - no mood detection (DM mood only changes via auto-rotation)")
# V2: Track message for autonomous engine (non-blocking, no LLM calls)
# IMPORTANT: Only call this if the message was NOT addressed to Miku
@@ -645,7 +653,7 @@ async def on_raw_reaction_add(payload):
)
reactor_type = "🤖 Miku" if is_bot_reactor else f"👤 {user.display_name}"
print(f" DM reaction added: {emoji_str} by {reactor_type} on message {payload.message_id}")
logger.debug(f"DM reaction added: {emoji_str} by {reactor_type} on message {payload.message_id}")
@globals.client.event
async def on_raw_reaction_remove(payload):
@@ -683,7 +691,7 @@ async def on_raw_reaction_remove(payload):
)
reactor_type = "🤖 Miku" if user.id == globals.client.user.id else f"👤 {user.display_name}"
print(f" DM reaction removed: {emoji_str} by {reactor_type} from message {payload.message_id}")
logger.debug(f"DM reaction removed: {emoji_str} by {reactor_type} from message {payload.message_id}")
@globals.client.event
async def on_presence_update(before, after):
@@ -698,16 +706,18 @@ async def on_member_join(member):
autonomous_member_join(member)
def start_api():
uvicorn.run(app, host="0.0.0.0", port=3939, log_level="info")
# Set log_level to "critical" to silence uvicorn's access logs
# Our custom api.requests middleware handles HTTP logging with better formatting and filtering
uvicorn.run(app, host="0.0.0.0", port=3939, log_level="critical")
def save_autonomous_state():
"""Save autonomous context on shutdown"""
try:
from utils.autonomous import autonomous_engine
autonomous_engine.save_context()
print("💾 Saved autonomous context on shutdown")
logger.info("💾 Saved autonomous context on shutdown")
except Exception as e:
print(f"⚠️ Failed to save autonomous context on shutdown: {e}")
logger.error(f"Failed to save autonomous context on shutdown: {e}")
# Register shutdown handlers
atexit.register(save_autonomous_state)

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -23,6 +23,9 @@ from utils.image_handling import (
convert_gif_to_mp4
)
from utils.sleep_responses import SLEEP_RESPONSES
from utils.logger import get_logger
logger = get_logger('autonomous')
# Server-specific memory storage
_server_autonomous_messages = {} # guild_id -> rotating buffer of last general messages
@@ -48,7 +51,7 @@ def save_autonomous_config(config):
def setup_autonomous_speaking():
"""Setup autonomous speaking for all configured servers"""
# This is now handled by the server manager
print("🤖 Autonomous Miku setup delegated to server manager!")
logger.debug("Autonomous Miku setup delegated to server manager!")
async def miku_autonomous_tick_for_server(guild_id: int, action_type="general", force=False, force_action=None):
"""Run autonomous behavior for a specific server"""
@@ -71,12 +74,12 @@ async def miku_say_something_general_for_server(guild_id: int):
"""Miku says something general in a specific server"""
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
channel = globals.client.get_channel(server_config.autonomous_channel_id)
if not channel:
print(f"⚠️ Autonomous channel not found for server {guild_id}")
logger.warning(f"Autonomous channel not found for server {guild_id}")
return
# Check if evil mode is active
@@ -123,7 +126,7 @@ async def miku_say_something_general_for_server(guild_id: int):
message = await query_llama(prompt, user_id=f"miku-autonomous-{guild_id}", guild_id=guild_id, response_type="autonomous_general")
if not is_too_similar(message, _server_autonomous_messages[guild_id]):
break
print("🔁 Response was too similar to past messages, retrying...")
logger.debug("Response was too similar to past messages, retrying...")
try:
await channel.send(message)
@@ -131,9 +134,9 @@ async def miku_say_something_general_for_server(guild_id: int):
if len(_server_autonomous_messages[guild_id]) > MAX_HISTORY:
_server_autonomous_messages[guild_id].pop(0)
character_name = "Evil Miku" if evil_mode else "Miku"
print(f"💬 {character_name} said something general in #{channel.name} (Server: {server_config.guild_name})")
logger.info(f"{character_name} said something general in #{channel.name} (Server: {server_config.guild_name})")
except Exception as e:
print(f"⚠️ Failed to send autonomous message: {e}")
logger.error(f"Failed to send autonomous message: {e}")
async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None, engagement_type: str = None):
"""Miku engages a random user in a specific server
@@ -145,17 +148,17 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
"""
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
guild = globals.client.get_guild(guild_id)
if not guild:
print(f"⚠️ Guild {guild_id} not found.")
logger.warning(f"Guild {guild_id} not found.")
return
channel = globals.client.get_channel(server_config.autonomous_channel_id)
if not channel:
print(f"⚠️ Autonomous channel not found for server {guild_id}")
logger.warning(f"Autonomous channel not found for server {guild_id}")
return
# Get target user
@@ -164,14 +167,14 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
try:
target = guild.get_member(int(user_id))
if not target:
print(f"⚠️ User {user_id} not found in server {guild_id}")
logger.warning(f"User {user_id} not found in server {guild_id}")
return
if target.bot:
print(f"⚠️ Cannot engage bot user {user_id}")
logger.warning(f"Cannot engage bot user {user_id}")
return
print(f"🎯 Targeting specific user: {target.display_name} (ID: {user_id})")
logger.info(f"Targeting specific user: {target.display_name} (ID: {user_id})")
except ValueError:
print(f"⚠️ Invalid user ID: {user_id}")
logger.warning(f"Invalid user ID: {user_id}")
return
else:
# Pick random user
@@ -181,11 +184,11 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
]
if not members:
print(f"😴 No available members to talk to in server {guild_id}.")
logger.warning(f"No available members to talk to in server {guild_id}.")
return
target = random.choice(members)
print(f"🎲 Randomly selected user: {target.display_name}")
logger.info(f"Randomly selected user: {target.display_name}")
time_of_day = get_time_of_day()
@@ -196,7 +199,7 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
now = time.time()
last_time = _server_user_engagements[guild_id].get(target.id, 0)
if now - last_time < 43200: # 12 hours in seconds
print(f"⏱️ Recently engaged {target.display_name} in server {guild_id}, switching to general message.")
logger.info(f"Recently engaged {target.display_name} in server {guild_id}, switching to general message.")
await miku_say_something_general_for_server(guild_id)
return
@@ -286,7 +289,7 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
)
if engagement_type:
print(f"💬 Engagement type: {engagement_type}")
logger.debug(f"Engagement type: {engagement_type}")
try:
# Use consistent user_id for engaging users to enable conversation history
@@ -294,9 +297,9 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
await channel.send(f"{target.mention} {message}")
_server_user_engagements[guild_id][target.id] = time.time()
character_name = "Evil Miku" if evil_mode else "Miku"
print(f"👤 {character_name} engaged {display_name} in server {server_config.guild_name}")
logger.info(f"{character_name} engaged {display_name} in server {server_config.guild_name}")
except Exception as e:
print(f"⚠️ Failed to engage user: {e}")
logger.error(f"Failed to engage user: {e}")
async def miku_detect_and_join_conversation_for_server(guild_id: int, force: bool = False):
"""Miku detects and joins conversations in a specific server
@@ -305,30 +308,30 @@ async def miku_detect_and_join_conversation_for_server(guild_id: int, force: boo
guild_id: The server ID
force: If True, bypass activity checks and random chance (for manual triggers)
"""
print(f"🔍 [Join Conv] Called for server {guild_id} (force={force})")
logger.debug(f"[Join Conv] Called for server {guild_id} (force={force})")
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
channel = globals.client.get_channel(server_config.autonomous_channel_id)
if not isinstance(channel, TextChannel):
print(f"⚠️ Autonomous channel is invalid or not found for server {guild_id}")
logger.warning(f"Autonomous channel is invalid or not found for server {guild_id}")
return
# Fetch last 20 messages (for filtering)
try:
messages = [msg async for msg in channel.history(limit=20)]
print(f"📜 [Join Conv] Fetched {len(messages)} messages from history")
logger.debug(f"[Join Conv] Fetched {len(messages)} messages from history")
except Exception as e:
print(f"⚠️ Failed to fetch channel history for server {guild_id}: {e}")
logger.error(f"Failed to fetch channel history for server {guild_id}: {e}")
return
# Filter messages based on force mode
if force:
# When forced, use messages from real users (no time limit) - but limit to last 10
recent_msgs = [msg for msg in messages if not msg.author.bot][:10]
print(f"📊 [Join Conv] Force mode: Using last {len(recent_msgs)} messages from users (no time limit)")
logger.debug(f"[Join Conv] Force mode: Using last {len(recent_msgs)} messages from users (no time limit)")
else:
# Normal mode: Filter to messages in last 10 minutes from real users (not bots)
recent_msgs = [
@@ -336,23 +339,23 @@ async def miku_detect_and_join_conversation_for_server(guild_id: int, force: boo
if not msg.author.bot
and (datetime.now(msg.created_at.tzinfo) - msg.created_at).total_seconds() < 600
]
print(f"📊 [Join Conv] Found {len(recent_msgs)} recent messages from users (last 10 min)")
logger.debug(f"[Join Conv] Found {len(recent_msgs)} recent messages from users (last 10 min)")
user_ids = set(msg.author.id for msg in recent_msgs)
if not force:
if len(recent_msgs) < 5 or len(user_ids) < 2:
# Not enough activity
print(f"⚠️ [Join Conv] Not enough activity: {len(recent_msgs)} messages, {len(user_ids)} users (need 5+ messages, 2+ users)")
logger.debug(f"[Join Conv] Not enough activity: {len(recent_msgs)} messages, {len(user_ids)} users (need 5+ messages, 2+ users)")
return
if random.random() > 0.5:
print(f"🎲 [Join Conv] Random chance failed (50% chance)")
logger.debug(f"[Join Conv] Random chance failed (50% chance)")
return # 50% chance to engage
else:
print(f"[Join Conv] Force mode - bypassing activity checks")
logger.debug(f"[Join Conv] Force mode - bypassing activity checks")
if len(recent_msgs) < 1:
print(f"⚠️ [Join Conv] No messages found in channel history")
logger.warning(f"[Join Conv] No messages found in channel history")
return
# Use last 10 messages for context (oldest to newest)
@@ -386,27 +389,27 @@ async def miku_detect_and_join_conversation_for_server(guild_id: int, force: boo
reply = await query_llama(prompt, user_id=f"miku-conversation-{guild_id}", guild_id=guild_id, response_type="conversation_join")
await channel.send(reply)
character_name = "Evil Miku" if evil_mode else "Miku"
print(f"💬 {character_name} joined an ongoing conversation in server {server_config.guild_name}")
logger.info(f"{character_name} joined an ongoing conversation in server {server_config.guild_name}")
except Exception as e:
print(f"⚠️ Failed to interject in conversation: {e}")
logger.error(f"Failed to interject in conversation: {e}")
async def share_miku_tweet_for_server(guild_id: int):
"""Share a Miku tweet in a specific server"""
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
channel = globals.client.get_channel(server_config.autonomous_channel_id)
tweets = await fetch_miku_tweets(limit=5)
if not tweets:
print(f"📭 No good tweets found for server {guild_id}")
logger.warning(f"No good tweets found for server {guild_id}")
return
fresh_tweets = [t for t in tweets if t["url"] not in LAST_SENT_TWEETS]
if not fresh_tweets:
print(f"⚠️ All fetched tweets were recently sent in server {guild_id}. Reusing tweets.")
logger.warning(f"All fetched tweets were recently sent in server {guild_id}. Reusing tweets.")
fresh_tweets = tweets
tweet = random.choice(fresh_tweets)
@@ -454,12 +457,12 @@ async def handle_custom_prompt_for_server(guild_id: int, user_prompt: str):
"""Handle custom prompt for a specific server"""
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return False
channel = globals.client.get_channel(server_config.autonomous_channel_id)
if not channel:
print(f"⚠️ Autonomous channel not found for server {guild_id}")
logger.warning(f"Autonomous channel not found for server {guild_id}")
return False
mood = server_config.current_mood_name
@@ -478,7 +481,7 @@ async def handle_custom_prompt_for_server(guild_id: int, user_prompt: str):
# Use consistent user_id for manual prompts to enable conversation history
message = await query_llama(prompt, user_id=f"miku-manual-{guild_id}", guild_id=guild_id, response_type="autonomous_general")
await channel.send(message)
print(f"🎤 Miku responded to custom prompt in server {server_config.guild_name}")
logger.info(f"Miku responded to custom prompt in server {server_config.guild_name}")
# Add to server-specific message history
if guild_id not in _server_autonomous_messages:
@@ -489,7 +492,7 @@ async def handle_custom_prompt_for_server(guild_id: int, user_prompt: str):
return True
except Exception as e:
print(f"Failed to send custom autonomous message: {e}")
logger.error(f"Failed to send custom autonomous message: {e}")
return False
# Legacy functions for backward compatibility - these now delegate to server-specific versions
@@ -542,7 +545,7 @@ def load_last_sent_tweets():
with open(LAST_SENT_TWEETS_FILE, "r", encoding="utf-8") as f:
LAST_SENT_TWEETS = json.load(f)
except Exception as e:
print(f"⚠️ Failed to load last sent tweets: {e}")
logger.error(f"Failed to load last sent tweets: {e}")
LAST_SENT_TWEETS = []
else:
LAST_SENT_TWEETS = []
@@ -552,7 +555,7 @@ def save_last_sent_tweets():
with open(LAST_SENT_TWEETS_FILE, "w", encoding="utf-8") as f:
json.dump(LAST_SENT_TWEETS, f)
except Exception as e:
print(f"⚠️ Failed to save last sent tweets: {e}")
logger.error(f"Failed to save last sent tweets: {e}")
def get_time_of_day():
hour = datetime.now().hour + 3
@@ -602,7 +605,7 @@ async def _analyze_message_media(message):
try:
# Handle images
if any(attachment.filename.lower().endswith(ext) for ext in [".jpg", ".jpeg", ".png", ".webp"]):
print(f" 📸 Analyzing image for reaction: {attachment.filename}")
logger.debug(f" Analyzing image for reaction: {attachment.filename}")
base64_img = await download_and_encode_image(attachment.url)
if base64_img:
description = await analyze_image_with_qwen(base64_img)
@@ -612,7 +615,7 @@ async def _analyze_message_media(message):
elif any(attachment.filename.lower().endswith(ext) for ext in [".gif", ".mp4", ".webm", ".mov"]):
is_gif = attachment.filename.lower().endswith('.gif')
media_type = "GIF" if is_gif else "video"
print(f" 🎬 Analyzing {media_type} for reaction: {attachment.filename}")
logger.debug(f" Analyzing {media_type} for reaction: {attachment.filename}")
# Download media
media_bytes_b64 = await download_and_encode_media(attachment.url)
@@ -635,7 +638,7 @@ async def _analyze_message_media(message):
return f"[{media_type}: {description}]"
except Exception as e:
print(f" ⚠️ Error analyzing media for reaction: {e}")
logger.warning(f" Error analyzing media for reaction: {e}")
continue
return None
@@ -650,25 +653,25 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
"""
# 50% chance to proceed (unless forced or with a specific message)
if not force and force_message is None and random.random() > 0.5:
print(f"🎲 Autonomous reaction skipped for server {guild_id} (50% chance)")
logger.debug(f"Autonomous reaction skipped for server {guild_id} (50% chance)")
return
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
server_name = server_config.guild_name
# Don't react if asleep
if server_config.current_mood_name == "asleep" or server_config.is_sleeping:
print(f"💤 [{server_name}] Miku is asleep, skipping autonomous reaction")
logger.info(f"[{server_name}] Miku is asleep, skipping autonomous reaction")
return
# Get the autonomous channel
channel = globals.client.get_channel(server_config.autonomous_channel_id)
if not channel:
print(f"⚠️ [{server_name}] Autonomous channel not found")
logger.warning(f"[{server_name}] Autonomous channel not found")
return
try:
@@ -677,9 +680,9 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
target_message = force_message
# Check if we've already reacted to this message
if target_message.id in _reacted_message_ids:
print(f"⏭️ [{server_name}] Already reacted to message {target_message.id}, skipping")
logger.debug(f"[{server_name}] Already reacted to message {target_message.id}, skipping")
return
print(f"🎯 [{server_name}] Reacting to new message from {target_message.author.display_name}")
logger.info(f"[{server_name}] Reacting to new message from {target_message.author.display_name}")
else:
# Fetch recent messages (last 50 messages to get more candidates)
messages = []
@@ -697,14 +700,14 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
messages.append(message)
if not messages:
print(f"📭 [{server_name}] No recent unreacted messages to react to")
logger.debug(f"[{server_name}] No recent unreacted messages to react to")
return
# Pick a random message from the recent ones
target_message = random.choice(messages)
# Analyze any media in the message
print(f"🔍 [{server_name}] Analyzing message for reaction from {target_message.author.display_name}")
logger.debug(f"[{server_name}] Analyzing message for reaction from {target_message.author.display_name}")
media_description = await _analyze_message_media(target_message)
# Build message content with media description if present
@@ -764,7 +767,7 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
emoji = emojis[0]
else:
# No emoji found in response, use fallback
print(f"⚠️ [{server_name}] LLM response contained no emoji: '{original_response[:50]}' - using fallback")
logger.warning(f"[{server_name}] LLM response contained no emoji: '{original_response[:50]}' - using fallback")
emoji = "💙"
# Final validation: try adding the reaction
@@ -772,7 +775,7 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
await target_message.add_reaction(emoji)
except discord.HTTPException as e:
if "Unknown Emoji" in str(e):
print(f"[{server_name}] Invalid emoji from LLM: '{original_response[:50]}' - using fallback")
logger.warning(f"[{server_name}] Invalid emoji from LLM: '{original_response[:50]}' - using fallback")
emoji = "💙"
await target_message.add_reaction(emoji)
else:
@@ -789,14 +792,14 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
for msg_id in ids_to_remove:
_reacted_message_ids.discard(msg_id)
print(f"[{server_name}] Autonomous reaction: Added {emoji} to message from {target_message.author.display_name}")
logger.info(f"[{server_name}] Autonomous reaction: Added {emoji} to message from {target_message.author.display_name}")
except discord.Forbidden:
print(f"[{server_name}] Missing permissions to add reactions")
logger.error(f"[{server_name}] Missing permissions to add reactions")
except discord.HTTPException as e:
print(f"[{server_name}] Failed to add reaction: {e}")
logger.error(f"[{server_name}] Failed to add reaction: {e}")
except Exception as e:
print(f"⚠️ [{server_name}] Error in autonomous reaction: {e}")
logger.error(f"[{server_name}] Error in autonomous reaction: {e}")
async def miku_autonomous_reaction(force=False):
"""Legacy function - run autonomous reactions for all servers
@@ -816,14 +819,14 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
"""
# 50% chance to proceed (unless forced with a specific message)
if force_message is None and random.random() > 0.5:
print(f"🎲 DM reaction skipped for user {user_id} (50% chance)")
logger.debug(f"DM reaction skipped for user {user_id} (50% chance)")
return
# Get the user object
try:
user = await globals.client.fetch_user(user_id)
if not user:
print(f"⚠️ Could not find user {user_id}")
logger.warning(f"Could not find user {user_id}")
return
dm_channel = user.dm_channel
@@ -833,7 +836,7 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
username = user.display_name
except Exception as e:
print(f"⚠️ Error fetching DM channel for user {user_id}: {e}")
logger.error(f"Error fetching DM channel for user {user_id}: {e}")
return
try:
@@ -842,9 +845,9 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
target_message = force_message
# Check if we've already reacted to this message
if target_message.id in _reacted_message_ids:
print(f"⏭️ [DM: {username}] Already reacted to message {target_message.id}, skipping")
logger.debug(f"[DM: {username}] Already reacted to message {target_message.id}, skipping")
return
print(f"🎯 [DM: {username}] Reacting to new message")
logger.info(f"[DM: {username}] Reacting to new message")
else:
# Fetch recent messages from DM (last 50 messages)
messages = []
@@ -862,14 +865,14 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
messages.append(message)
if not messages:
print(f"📭 [DM: {username}] No recent unreacted messages to react to")
logger.debug(f"[DM: {username}] No recent unreacted messages to react to")
return
# Pick a random message from the recent ones
target_message = random.choice(messages)
# Analyze any media in the message
print(f"🔍 [DM: {username}] Analyzing message for reaction")
logger.debug(f"[DM: {username}] Analyzing message for reaction")
media_description = await _analyze_message_media(target_message)
# Build message content with media description if present
@@ -929,7 +932,7 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
emoji = emojis[0]
else:
# No emoji found in response, use fallback
print(f"⚠️ [DM: {username}] LLM response contained no emoji: '{original_response[:50]}' - using fallback")
logger.warning(f"[DM: {username}] LLM response contained no emoji: '{original_response[:50]}' - using fallback")
emoji = "💙"
# Final validation: try adding the reaction
@@ -937,7 +940,7 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
await target_message.add_reaction(emoji)
except discord.HTTPException as e:
if "Unknown Emoji" in str(e):
print(f"[DM: {username}] Invalid emoji from LLM: '{original_response[:50]}' - using fallback")
logger.warning(f"[DM: {username}] Invalid emoji from LLM: '{original_response[:50]}' - using fallback")
emoji = "💙"
await target_message.add_reaction(emoji)
else:
@@ -954,14 +957,14 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
for msg_id in ids_to_remove:
_reacted_message_ids.discard(msg_id)
print(f"[DM: {username}] Autonomous reaction: Added {emoji} to message")
logger.info(f"[DM: {username}] Autonomous reaction: Added {emoji} to message")
except discord.Forbidden:
print(f"[DM: {username}] Missing permissions to add reactions")
logger.error(f"[DM: {username}] Missing permissions to add reactions")
except discord.HTTPException as e:
print(f"[DM: {username}] Failed to add reaction: {e}")
logger.error(f"[DM: {username}] Failed to add reaction: {e}")
except Exception as e:
print(f"⚠️ [DM: {username}] Error in autonomous reaction: {e}")
logger.error(f"[DM: {username}] Error in autonomous reaction: {e}")
async def miku_update_profile_picture_for_server(guild_id: int):
@@ -973,18 +976,18 @@ async def miku_update_profile_picture_for_server(guild_id: int):
# Check if enough time has passed
if not should_update_profile_picture():
print(f"📸 [Server: {guild_id}] Profile picture not ready for update yet")
logger.debug(f"[Server: {guild_id}] Profile picture not ready for update yet")
return
# Get server config to use current mood
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
mood = server_config.current_mood_name
print(f"📸 [Server: {guild_id}] Attempting profile picture update (mood: {mood})")
logger.info(f"[Server: {guild_id}] Attempting profile picture update (mood: {mood})")
try:
success = await update_profile_picture(globals.client, mood=mood)
@@ -1001,9 +1004,9 @@ async def miku_update_profile_picture_for_server(guild_id: int):
"*updates avatar* Time for a fresh look! ✨"
]
await channel.send(random.choice(messages))
print(f"[Server: {guild_id}] Profile picture updated and announced!")
logger.info(f"[Server: {guild_id}] Profile picture updated and announced!")
else:
print(f"⚠️ [Server: {guild_id}] Profile picture update failed")
logger.warning(f"[Server: {guild_id}] Profile picture update failed")
except Exception as e:
print(f"⚠️ [Server: {guild_id}] Error updating profile picture: {e}")
logger.error(f"[Server: {guild_id}] Error updating profile picture: {e}")

View File

@@ -11,6 +11,9 @@ import random
import asyncio
import discord
import globals
from utils.logger import get_logger
logger = get_logger('persona')
# ============================================================================
# CONSTANTS
@@ -38,26 +41,26 @@ def save_bipolar_state():
}
with open(BIPOLAR_STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2)
print(f"💾 Saved bipolar mode state: enabled={globals.BIPOLAR_MODE}")
logger.info(f"Saved bipolar mode state: enabled={globals.BIPOLAR_MODE}")
except Exception as e:
print(f"⚠️ Failed to save bipolar mode state: {e}")
logger.error(f"Failed to save bipolar mode state: {e}")
def load_bipolar_state():
"""Load bipolar mode state from JSON file"""
try:
if not os.path.exists(BIPOLAR_STATE_FILE):
print(" No bipolar mode state file found, using defaults")
logger.info("No bipolar mode state file found, using defaults")
return False
with open(BIPOLAR_STATE_FILE, "r", encoding="utf-8") as f:
state = json.load(f)
bipolar_mode = state.get("bipolar_mode_enabled", False)
print(f"📂 Loaded bipolar mode state: enabled={bipolar_mode}")
logger.info(f"Loaded bipolar mode state: enabled={bipolar_mode}")
return bipolar_mode
except Exception as e:
print(f"⚠️ Failed to load bipolar mode state: {e}")
logger.error(f"Failed to load bipolar mode state: {e}")
return False
@@ -71,16 +74,16 @@ def save_webhooks():
with open(BIPOLAR_WEBHOOKS_FILE, "w", encoding="utf-8") as f:
json.dump(webhooks_data, f, indent=2)
print(f"💾 Saved bipolar webhooks for {len(webhooks_data)} server(s)")
logger.info(f"Saved bipolar webhooks for {len(webhooks_data)} server(s)")
except Exception as e:
print(f"⚠️ Failed to save bipolar webhooks: {e}")
logger.error(f"Failed to save bipolar webhooks: {e}")
def load_webhooks():
"""Load webhook URLs from JSON file"""
try:
if not os.path.exists(BIPOLAR_WEBHOOKS_FILE):
print(" No bipolar webhooks file found")
logger.info("No bipolar webhooks file found")
return {}
with open(BIPOLAR_WEBHOOKS_FILE, "r", encoding="utf-8") as f:
@@ -91,10 +94,10 @@ def load_webhooks():
for guild_id_str, webhook_data in webhooks_data.items():
webhooks[int(guild_id_str)] = webhook_data
print(f"📂 Loaded bipolar webhooks for {len(webhooks)} server(s)")
logger.info(f"Loaded bipolar webhooks for {len(webhooks)} server(s)")
return webhooks
except Exception as e:
print(f"⚠️ Failed to load bipolar webhooks: {e}")
logger.error(f"Failed to load bipolar webhooks: {e}")
return {}
@@ -105,8 +108,8 @@ def restore_bipolar_mode_on_startup():
globals.BIPOLAR_WEBHOOKS = load_webhooks()
if bipolar_mode:
print("🔄 Bipolar mode restored from previous session")
print("💬 Persona dialogue system enabled (natural conversations + arguments)")
logger.info("Bipolar mode restored from previous session")
logger.info("Persona dialogue system enabled (natural conversations + arguments)")
return bipolar_mode
@@ -124,7 +127,7 @@ def load_scoreboard() -> dict:
with open(BIPOLAR_SCOREBOARD_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"⚠️ Failed to load scoreboard: {e}")
logger.error(f"Failed to load scoreboard: {e}")
return {"miku": 0, "evil": 0, "history": []}
@@ -134,9 +137,9 @@ def save_scoreboard(scoreboard: dict):
os.makedirs(os.path.dirname(BIPOLAR_SCOREBOARD_FILE), exist_ok=True)
with open(BIPOLAR_SCOREBOARD_FILE, "w", encoding="utf-8") as f:
json.dump(scoreboard, f, indent=2)
print(f"💾 Saved scoreboard: Miku {scoreboard['miku']} - {scoreboard['evil']} Evil Miku")
logger.info(f"Saved scoreboard: Miku {scoreboard['miku']} - {scoreboard['evil']} Evil Miku")
except Exception as e:
print(f"⚠️ Failed to save scoreboard: {e}")
logger.error(f"Failed to save scoreboard: {e}")
def record_argument_result(winner: str, exchanges: int, reasoning: str = ""):
@@ -205,7 +208,7 @@ def enable_bipolar_mode():
"""Enable bipolar mode"""
globals.BIPOLAR_MODE = True
save_bipolar_state()
print("🔄 Bipolar mode enabled!")
logger.info("Bipolar mode enabled!")
def disable_bipolar_mode():
@@ -214,7 +217,7 @@ def disable_bipolar_mode():
# Clear any ongoing arguments
globals.BIPOLAR_ARGUMENT_IN_PROGRESS.clear()
save_bipolar_state()
print("🔄 Bipolar mode disabled!")
logger.info("Bipolar mode disabled!")
def toggle_bipolar_mode() -> bool:
@@ -256,11 +259,11 @@ async def get_or_create_webhooks_for_channel(channel: discord.TextChannel) -> di
if miku_webhook and evil_webhook:
return {"miku": miku_webhook, "evil_miku": evil_webhook}
except Exception as e:
print(f"⚠️ Failed to retrieve cached webhooks: {e}")
logger.warning(f"Failed to retrieve cached webhooks: {e}")
# Create new webhooks
try:
print(f"🔧 Creating bipolar webhooks for channel #{channel.name}")
logger.info(f"Creating bipolar webhooks for channel #{channel.name}")
# Load avatar images
miku_avatar = None
@@ -300,14 +303,14 @@ async def get_or_create_webhooks_for_channel(channel: discord.TextChannel) -> di
}
save_webhooks()
print(f"Created bipolar webhooks for #{channel.name}")
logger.info(f"Created bipolar webhooks for #{channel.name}")
return {"miku": miku_webhook, "evil_miku": evil_webhook}
except discord.Forbidden:
print(f"Missing permissions to create webhooks in #{channel.name}")
logger.error(f"Missing permissions to create webhooks in #{channel.name}")
return None
except Exception as e:
print(f"Failed to create webhooks: {e}")
logger.error(f"Failed to create webhooks: {e}")
return None
@@ -322,11 +325,11 @@ async def cleanup_webhooks(client):
await webhook.delete(reason="Bipolar mode cleanup")
cleaned_count += 1
except Exception as e:
print(f"⚠️ Failed to cleanup webhooks in {guild.name}: {e}")
logger.warning(f"Failed to cleanup webhooks in {guild.name}: {e}")
globals.BIPOLAR_WEBHOOKS.clear()
save_webhooks()
print(f"🧹 Cleaned up {cleaned_count} bipolar webhook(s)")
logger.info(f"Cleaned up {cleaned_count} bipolar webhook(s)")
return cleaned_count
@@ -602,7 +605,7 @@ async def judge_argument_winner(conversation_log: list, guild_id: int) -> tuple[
)
if not judgment or judgment.startswith("Error"):
print("⚠️ Arbiter failed to make judgment, defaulting to draw")
logger.warning("Arbiter failed to make judgment, defaulting to draw")
return "draw", "The arbiter could not make a decision."
# Parse the judgment - look at the first line/sentence for the decision
@@ -610,37 +613,37 @@ async def judge_argument_winner(conversation_log: list, guild_id: int) -> tuple[
first_line = judgment_lines[0].strip().strip('"').strip()
first_line_lower = first_line.lower()
print(f"🔍 Parsing arbiter first line: '{first_line}'")
logger.debug(f"Parsing arbiter first line: '{first_line}'")
# Check the first line for the decision - be very specific
# The arbiter should respond with ONLY the name on the first line
if first_line_lower == "evil miku":
winner = "evil"
print("Detected Evil Miku win from first line exact match")
logger.debug("Detected Evil Miku win from first line exact match")
elif first_line_lower == "hatsune miku":
winner = "miku"
print("Detected Hatsune Miku win from first line exact match")
logger.debug("Detected Hatsune Miku win from first line exact match")
elif first_line_lower == "draw":
winner = "draw"
print("Detected Draw from first line exact match")
logger.debug("Detected Draw from first line exact match")
elif "evil miku" in first_line_lower and "hatsune" not in first_line_lower:
# First line mentions Evil Miku but not Hatsune Miku
winner = "evil"
print("Detected Evil Miku win from first line (contains 'evil miku' only)")
logger.debug("Detected Evil Miku win from first line (contains 'evil miku' only)")
elif "hatsune miku" in first_line_lower and "evil" not in first_line_lower:
# First line mentions Hatsune Miku but not Evil Miku
winner = "miku"
print("Detected Hatsune Miku win from first line (contains 'hatsune miku' only)")
logger.debug("Detected Hatsune Miku win from first line (contains 'hatsune miku' only)")
else:
# Fallback: check the whole judgment
print(f"⚠️ First line ambiguous, using fallback counting method")
logger.debug(f"First line ambiguous, using fallback counting method")
judgment_lower = judgment.lower()
# Count mentions to break ties
evil_count = judgment_lower.count("evil miku")
miku_count = judgment_lower.count("hatsune miku")
draw_count = judgment_lower.count("draw")
print(f"📊 Counts - Evil: {evil_count}, Miku: {miku_count}, Draw: {draw_count}")
logger.debug(f"Counts - Evil: {evil_count}, Miku: {miku_count}, Draw: {draw_count}")
if draw_count > 0 and draw_count >= evil_count and draw_count >= miku_count:
winner = "draw"
@@ -654,7 +657,7 @@ async def judge_argument_winner(conversation_log: list, guild_id: int) -> tuple[
return winner, judgment
except Exception as e:
print(f"⚠️ Error in arbiter judgment: {e}")
logger.error(f"Error in arbiter judgment: {e}")
return "draw", "An error occurred during judgment."
@@ -756,13 +759,13 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
guild_id = channel.guild.id
if is_argument_in_progress(channel_id):
print(f"⚠️ Argument already in progress in #{channel.name}")
logger.warning(f"Argument already in progress in #{channel.name}")
return
# Get webhooks for this channel
webhooks = await get_or_create_webhooks_for_channel(channel)
if not webhooks:
print(f"Could not create webhooks for argument in #{channel.name}")
logger.error(f"Could not create webhooks for argument in #{channel.name}")
return
# Determine who initiates based on starting_message or inactive persona
@@ -773,12 +776,12 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
is_evil_message = globals.EVIL_MODE or (starting_message.webhook_id is not None and "Evil" in (starting_message.author.name or ""))
initiator = "miku" if is_evil_message else "evil" # Opposite persona responds
last_message = starting_message.content
print(f"🔄 Starting argument from message, responder: {initiator}")
logger.info(f"Starting argument from message, responder: {initiator}")
else:
# The inactive persona breaks through
initiator = get_inactive_persona()
last_message = None
print(f"🔄 Starting bipolar argument in #{channel.name}, initiated by {initiator}")
logger.info(f"Starting bipolar argument in #{channel.name}, initiated by {initiator}")
start_argument(channel_id, initiator)
@@ -812,7 +815,7 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
globals.EVIL_MODE = original_evil_mode
if not initial_message or initial_message.startswith("Error") or initial_message.startswith("Sorry"):
print("Failed to generate initial argument message")
logger.error("Failed to generate initial argument message")
end_argument(channel_id)
return
@@ -877,22 +880,22 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
if should_end:
exchange_count = globals.BIPOLAR_ARGUMENT_IN_PROGRESS.get(channel_id, {}).get("exchange_count", 0)
print(f"⚖️ Argument complete with {exchange_count} exchanges. Calling arbiter...")
logger.info(f"Argument complete with {exchange_count} exchanges. Calling arbiter...")
# Use arbiter to judge the winner
winner, judgment = await judge_argument_winner(conversation_log, guild_id)
print(f"⚖️ Arbiter decision: {winner}")
print(f"📝 Judgment: {judgment}")
logger.info(f"Arbiter decision: {winner}")
logger.info(f"Judgment: {judgment}")
# If it's a draw, continue the argument instead of ending
if winner == "draw":
print("🤝 Arbiter ruled it's still a draw - argument continues...")
logger.info("Arbiter ruled it's still a draw - argument continues...")
# Reduce the end chance by 5% (but don't go below 5%)
current_end_chance = globals.BIPOLAR_ARGUMENT_IN_PROGRESS[channel_id].get("end_chance", 0.1)
new_end_chance = max(0.05, current_end_chance - 0.05)
globals.BIPOLAR_ARGUMENT_IN_PROGRESS[channel_id]["end_chance"] = new_end_chance
print(f"📉 Reduced end chance to {new_end_chance*100:.0f}% - argument continues...")
logger.info(f"Reduced end chance to {new_end_chance*100:.0f}% - argument continues...")
# Don't end, just continue to the next exchange
else:
# Clear winner - generate final triumphant message
@@ -938,10 +941,10 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
# Switch to winner's mode (including role color)
from utils.evil_mode import apply_evil_mode_changes, revert_evil_mode_changes
if winner == "evil":
print("👿 Evil Miku won! Switching to Evil Mode...")
logger.info("Evil Miku won! Switching to Evil Mode...")
await apply_evil_mode_changes(client, change_username=True, change_pfp=True, change_nicknames=True, change_role_color=True)
else:
print("💙 Hatsune Miku won! Switching to Normal Mode...")
logger.info("Hatsune Miku won! Switching to Normal Mode...")
await revert_evil_mode_changes(client, change_username=True, change_pfp=True, change_nicknames=True, change_role_color=True)
# Clean up argument conversation history
@@ -951,7 +954,7 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
pass # History cleanup is not critical
end_argument(channel_id)
print(f"Argument ended in #{channel.name}, winner: {winner}")
logger.info(f"Argument ended in #{channel.name}, winner: {winner}")
return
# Get current speaker
@@ -982,7 +985,7 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
globals.EVIL_MODE = original_evil_mode
if not response or response.startswith("Error") or response.startswith("Sorry"):
print(f"Failed to generate argument response")
logger.error(f"Failed to generate argument response")
end_argument(channel_id)
return
@@ -1021,7 +1024,7 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
is_first_response = False
except Exception as e:
print(f"Argument error: {e}")
logger.error(f"Argument error: {e}")
import traceback
traceback.print_exc()
end_argument(channel_id)
@@ -1057,11 +1060,11 @@ async def force_trigger_argument(channel: discord.TextChannel, client, context:
starting_message: Optional message to use as the first message in the argument
"""
if not globals.BIPOLAR_MODE:
print("⚠️ Cannot trigger argument - bipolar mode is not enabled")
logger.warning("Cannot trigger argument - bipolar mode is not enabled")
return False
if is_argument_in_progress(channel.id):
print("⚠️ Argument already in progress in this channel")
logger.warning("Argument already in progress in this channel")
return False
asyncio.create_task(run_argument(channel, client, context, starting_message))

View File

@@ -5,13 +5,18 @@ Replaces the vector search system with organized, complete context.
Preserves original content files in their entirety.
"""
from utils.logger import get_logger
logger = get_logger('core')
def get_original_miku_lore() -> str:
"""Load the complete, unmodified miku_lore.txt file"""
try:
with open("miku_lore.txt", "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"⚠️ Failed to load miku_lore.txt: {e}")
logger.error(f"Failed to load miku_lore.txt: {e}")
return "## MIKU LORE\n[File could not be loaded]"
@@ -21,7 +26,7 @@ def get_original_miku_prompt() -> str:
with open("miku_prompt.txt", "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"⚠️ Failed to load miku_prompt.txt: {e}")
logger.error(f"Failed to load miku_prompt.txt: {e}")
return "## MIKU PROMPT\n[File could not be loaded]"
@@ -31,7 +36,7 @@ def get_original_miku_lyrics() -> str:
with open("miku_lyrics.txt", "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"⚠️ Failed to load miku_lyrics.txt: {e}")
logger.error(f"Failed to load miku_lyrics.txt: {e}")
return "## MIKU LYRICS\n[File could not be loaded]"

View File

@@ -8,6 +8,9 @@ import globals
from langchain_community.vectorstores import FAISS
from langchain_text_splitters import CharacterTextSplitter, RecursiveCharacterTextSplitter
from langchain_core.documents import Document
from utils.logger import get_logger
logger = get_logger('core')
# switch_model() removed - llama-swap handles model switching automatically
@@ -21,7 +24,7 @@ async def is_miku_addressed(message) -> bool:
# Safety check: ensure guild and guild.me exist
if not message.guild or not message.guild.me:
print(f"⚠️ Warning: Invalid guild or guild.me in message from {message.author}")
logger.warning(f"Invalid guild or guild.me in message from {message.author}")
return False
# If message contains a ping for Miku, return true
@@ -35,7 +38,7 @@ async def is_miku_addressed(message) -> bool:
if referenced_msg.author == message.guild.me:
return True
except Exception as e:
print(f"⚠️ Could not fetch referenced message: {e}")
logger.warning(f"Could not fetch referenced message: {e}")
cleaned = message.content.strip()

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,9 @@ import os
import random
import json
import globals
from utils.logger import get_logger
logger = get_logger('persona')
# ============================================================================
# EVIL MODE PERSISTENCE
@@ -40,16 +43,16 @@ def save_evil_mode_state(saved_role_color=None):
}
with open(EVIL_MODE_STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2)
print(f"💾 Saved evil mode state: {state}")
logger.debug(f"Saved evil mode state: {state}")
except Exception as e:
print(f"⚠️ Failed to save evil mode state: {e}")
logger.error(f"Failed to save evil mode state: {e}")
def load_evil_mode_state():
"""Load evil mode state from JSON file"""
try:
if not os.path.exists(EVIL_MODE_STATE_FILE):
print(f" No evil mode state file found, using defaults")
logger.info(f"No evil mode state file found, using defaults")
return False, "evil_neutral", None
with open(EVIL_MODE_STATE_FILE, "r", encoding="utf-8") as f:
@@ -58,10 +61,10 @@ def load_evil_mode_state():
evil_mode = state.get("evil_mode_enabled", False)
evil_mood = state.get("evil_mood", "evil_neutral")
saved_role_color = state.get("saved_role_color")
print(f"📂 Loaded evil mode state: evil_mode={evil_mode}, mood={evil_mood}, saved_color={saved_role_color}")
logger.debug(f"Loaded evil mode state: evil_mode={evil_mode}, mood={evil_mood}, saved_color={saved_role_color}")
return evil_mode, evil_mood, saved_role_color
except Exception as e:
print(f"⚠️ Failed to load evil mode state: {e}")
logger.error(f"Failed to load evil mode state: {e}")
return False, "evil_neutral", None
@@ -70,13 +73,13 @@ def restore_evil_mode_on_startup():
evil_mode, evil_mood, saved_role_color = load_evil_mode_state()
if evil_mode:
print("😈 Restoring evil mode from previous session...")
logger.debug("Restoring evil mode from previous session...")
globals.EVIL_MODE = True
globals.EVIL_DM_MOOD = evil_mood
globals.EVIL_DM_MOOD_DESCRIPTION = load_evil_mood_description(evil_mood)
print(f"😈 Evil mode restored: {evil_mood}")
logger.info(f"Evil mode restored: {evil_mood}")
else:
print("🎤 Normal mode active")
logger.info("Normal mode active")
return evil_mode
@@ -90,7 +93,7 @@ def get_evil_miku_lore() -> str:
with open("evil_miku_lore.txt", "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"⚠️ Failed to load evil_miku_lore.txt: {e}")
logger.error(f"Failed to load evil_miku_lore.txt: {e}")
return "## EVIL MIKU LORE\n[File could not be loaded]"
@@ -100,7 +103,7 @@ def get_evil_miku_prompt() -> str:
with open("evil_miku_prompt.txt", "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"⚠️ Failed to load evil_miku_prompt.txt: {e}")
logger.error(f"Failed to load evil_miku_prompt.txt: {e}")
return "## EVIL MIKU PROMPT\n[File could not be loaded]"
@@ -110,7 +113,7 @@ def get_evil_miku_lyrics() -> str:
with open("evil_miku_lyrics.txt", "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"⚠️ Failed to load evil_miku_lyrics.txt: {e}")
logger.error(f"Failed to load evil_miku_lyrics.txt: {e}")
return "## EVIL MIKU LYRICS\n[File could not be loaded]"
@@ -178,7 +181,7 @@ def load_evil_mood_description(mood_name: str) -> str:
with open(path, "r", encoding="utf-8") as f:
return f.read().strip()
except FileNotFoundError:
print(f"⚠️ Evil mood file '{mood_name}' not found. Falling back to evil_neutral.")
logger.warning(f"Evil mood file '{mood_name}' not found. Falling back to evil_neutral.")
try:
with open(os.path.join("moods", "evil", "evil_neutral.txt"), "r", encoding="utf-8") as f:
return f.read().strip()
@@ -338,13 +341,13 @@ async def get_current_role_color(client) -> str:
if role.name.lower() in ["miku color", "miku colour", "miku-color"]:
# Convert discord.Color to hex
hex_color = f"#{role.color.value:06x}"
print(f"🎨 Current role color: {hex_color}")
logger.debug(f"Current role color: {hex_color}")
return hex_color
print("⚠️ No 'Miku Color' role found in any server")
logger.warning("No 'Miku Color' role found in any server")
return None
except Exception as e:
print(f"⚠️ Failed to get current role color: {e}")
logger.warning(f"Failed to get current role color: {e}")
return None
@@ -377,14 +380,14 @@ async def set_role_color(client, hex_color: str):
if color_role:
await color_role.edit(color=discord_color, reason="Evil mode color change")
updated_count += 1
print(f" 🎨 Updated role color in {guild.name}: #{hex_color}")
logger.debug(f"Updated role color in {guild.name}: #{hex_color}")
except Exception as e:
print(f" ⚠️ Failed to update role color in {guild.name}: {e}")
logger.warning(f"Failed to update role color in {guild.name}: {e}")
print(f"🎨 Updated role color in {updated_count} server(s) to #{hex_color}")
logger.info(f"Updated role color in {updated_count} server(s) to #{hex_color}")
return updated_count > 0
except Exception as e:
print(f"⚠️ Failed to set role color: {e}")
logger.error(f"Failed to set role color: {e}")
return False
@@ -398,7 +401,7 @@ async def apply_evil_mode_changes(client, change_username=True, change_pfp=True,
change_nicknames: Whether to change server nicknames (default True, but skip on startup restore)
change_role_color: Whether to change role color (default True, but skip on startup restore)
"""
print("😈 Enabling Evil Mode...")
logger.info("Enabling Evil Mode...")
# Save current role color before changing (if we're actually changing it)
if change_role_color:
@@ -412,9 +415,9 @@ async def apply_evil_mode_changes(client, change_username=True, change_pfp=True,
if change_username:
try:
await client.user.edit(username="Evil Miku")
print("Changed bot username to 'Evil Miku'")
logger.debug("Changed bot username to 'Evil Miku'")
except Exception as e:
print(f"⚠️ Could not change bot username: {e}")
logger.error(f"Could not change bot username: {e}")
# Update nicknames in all servers
if change_nicknames:
@@ -431,7 +434,7 @@ async def apply_evil_mode_changes(client, change_username=True, change_pfp=True,
# Save state to file
save_evil_mode_state()
print("😈 Evil Mode enabled!")
logger.info("Evil Mode enabled!")
async def revert_evil_mode_changes(client, change_username=True, change_pfp=True, change_nicknames=True, change_role_color=True):
@@ -444,16 +447,16 @@ async def revert_evil_mode_changes(client, change_username=True, change_pfp=True
change_nicknames: Whether to change server nicknames (default True, but skip on startup restore)
change_role_color: Whether to restore role color (default True, but skip on startup restore)
"""
print("🎤 Disabling Evil Mode...")
logger.info("Disabling Evil Mode...")
globals.EVIL_MODE = False
# Change bot username back
if change_username:
try:
await client.user.edit(username="Hatsune Miku")
print("Changed bot username back to 'Hatsune Miku'")
logger.debug("Changed bot username back to 'Hatsune Miku'")
except Exception as e:
print(f"⚠️ Could not change bot username: {e}")
logger.error(f"Could not change bot username: {e}")
# Update nicknames in all servers back to normal
if change_nicknames:
@@ -469,16 +472,16 @@ async def revert_evil_mode_changes(client, change_username=True, change_pfp=True
_, _, saved_color = load_evil_mode_state()
if saved_color:
await set_role_color(client, saved_color)
print(f"🎨 Restored role color to {saved_color}")
logger.debug(f"Restored role color to {saved_color}")
else:
print("⚠️ No saved role color found, skipping color restoration")
logger.warning("No saved role color found, skipping color restoration")
except Exception as e:
print(f"⚠️ Failed to restore role color: {e}")
logger.error(f"Failed to restore role color: {e}")
# Save state to file (this will clear saved_role_color since we're back to normal)
save_evil_mode_state(saved_role_color=None)
print("🎤 Evil Mode disabled!")
logger.info("Evil Mode disabled!")
async def update_all_evil_nicknames(client):
@@ -505,9 +508,9 @@ async def update_evil_server_nickname(client, guild_id: int):
me = guild.get_member(client.user.id)
if me:
await me.edit(nick=nickname)
print(f"😈 Changed nickname to '{nickname}' in server {guild.name}")
logger.debug(f"Changed nickname to '{nickname}' in server {guild.name}")
except Exception as e:
print(f"⚠️ Failed to update evil nickname in guild {guild_id}: {e}")
logger.error(f"Failed to update evil nickname in guild {guild_id}: {e}")
async def revert_all_nicknames(client):
@@ -524,7 +527,7 @@ async def set_evil_profile_picture(client):
evil_pfp_path = "memory/profile_pictures/evil_pfp.png"
if not os.path.exists(evil_pfp_path):
print(f"⚠️ Evil profile picture not found at {evil_pfp_path}")
logger.error(f"Evil profile picture not found at {evil_pfp_path}")
return False
try:
@@ -532,10 +535,10 @@ async def set_evil_profile_picture(client):
avatar_bytes = f.read()
await client.user.edit(avatar=avatar_bytes)
print("😈 Set evil profile picture")
logger.debug("Set evil profile picture")
return True
except Exception as e:
print(f"⚠️ Failed to set evil profile picture: {e}")
logger.error(f"Failed to set evil profile picture: {e}")
return False
@@ -554,12 +557,12 @@ async def restore_normal_profile_picture(client):
avatar_bytes = f.read()
await client.user.edit(avatar=avatar_bytes)
print(f"🎤 Restored normal profile picture from {path}")
logger.debug(f"Restored normal profile picture from {path}")
return True
except Exception as e:
print(f"⚠️ Failed to restore from {path}: {e}")
logger.error(f"Failed to restore from {path}: {e}")
print("⚠️ Could not restore normal profile picture - no backup found")
logger.error("Could not restore normal profile picture - no backup found")
return False
@@ -602,4 +605,4 @@ async def rotate_evil_mood():
globals.EVIL_DM_MOOD_DESCRIPTION = load_evil_mood_description(new_mood)
save_evil_mode_state() # Save state when mood rotates
print(f"😈 Evil mood rotated from {old_mood} to {new_mood}")
logger.info(f"Evil mood rotated from {old_mood} to {new_mood}")

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

View File

@@ -10,7 +10,9 @@ import globals
from utils.twitter_fetcher import fetch_figurine_tweets_latest
from utils.image_handling import analyze_image_with_qwen, download_and_encode_image
from utils.llm import query_llama
from utils.logger import get_logger
logger = get_logger('bot')
from utils.dm_logger import dm_logger
@@ -37,14 +39,14 @@ def _ensure_dir(path: str) -> None:
def load_subscribers() -> List[int]:
try:
if os.path.exists(SUBSCRIBERS_FILE):
print(f"📁 Figurines: Loading subscribers from {SUBSCRIBERS_FILE}")
logger.debug(f"Loading subscribers from {SUBSCRIBERS_FILE}")
with open(SUBSCRIBERS_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
subs = [int(uid) for uid in data.get("subscribers", [])]
print(f"📋 Figurines: Loaded {len(subs)} subscribers")
logger.debug(f"Loaded {len(subs)} subscribers")
return subs
except Exception as e:
print(f"⚠️ Failed to load figurine subscribers: {e}")
logger.error(f"Failed to load figurine subscribers: {e}")
return []
@@ -53,85 +55,85 @@ def save_subscribers(user_ids: List[int]) -> None:
_ensure_dir(SUBSCRIBERS_FILE)
# Save as strings to be JS-safe in the API layer if needed
payload = {"subscribers": [str(uid) for uid in user_ids]}
print(f"💾 Figurines: Saving {len(user_ids)} subscribers to {SUBSCRIBERS_FILE}")
logger.debug(f"Saving {len(user_ids)} subscribers to {SUBSCRIBERS_FILE}")
with open(SUBSCRIBERS_FILE, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2)
except Exception as e:
print(f"⚠️ Failed to save figurine subscribers: {e}")
logger.error(f"Failed to save figurine subscribers: {e}")
def add_subscriber(user_id: int) -> bool:
print(f" Figurines: Adding subscriber {user_id}")
logger.info(f"Adding subscriber {user_id}")
subscribers = load_subscribers()
if user_id in subscribers:
print(f" Figurines: Subscriber {user_id} already present")
logger.info(f"Subscriber {user_id} already present")
return False
subscribers.append(user_id)
save_subscribers(subscribers)
print(f"✅ Figurines: Subscriber {user_id} added")
logger.info(f"Subscriber {user_id} added")
return True
def remove_subscriber(user_id: int) -> bool:
print(f"🗑️ Figurines: Removing subscriber {user_id}")
logger.info(f"Removing subscriber {user_id}")
subscribers = load_subscribers()
if user_id not in subscribers:
print(f" Figurines: Subscriber {user_id} was not present")
logger.info(f"Subscriber {user_id} was not present")
return False
subscribers = [uid for uid in subscribers if uid != user_id]
save_subscribers(subscribers)
print(f"✅ Figurines: Subscriber {user_id} removed")
logger.info(f"Subscriber {user_id} removed")
return True
def load_sent_tweets() -> List[str]:
try:
if os.path.exists(SENT_TWEETS_FILE):
print(f"📁 Figurines: Loading sent tweets from {SENT_TWEETS_FILE}")
logger.debug(f"Loading sent tweets from {SENT_TWEETS_FILE}")
with open(SENT_TWEETS_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
urls = data.get("urls", [])
print(f"📋 Figurines: Loaded {len(urls)} sent tweet URLs")
logger.debug(f"Loaded {len(urls)} sent tweet URLs")
return urls
except Exception as e:
print(f"⚠️ Failed to load figurine sent tweets: {e}")
logger.error(f"Failed to load figurine sent tweets: {e}")
return []
def save_sent_tweets(urls: List[str]) -> None:
try:
_ensure_dir(SENT_TWEETS_FILE)
print(f"💾 Figurines: Saving {len(urls)} sent tweet URLs to {SENT_TWEETS_FILE}")
logger.debug(f"Saving {len(urls)} sent tweet URLs to {SENT_TWEETS_FILE}")
with open(SENT_TWEETS_FILE, "w", encoding="utf-8") as f:
json.dump({"urls": urls}, f, indent=2)
except Exception as e:
print(f"⚠️ Failed to save figurine sent tweets: {e}")
logger.error(f"Failed to save figurine sent tweets: {e}")
async def choose_random_figurine_tweet() -> Dict[str, Any] | None:
"""Fetch figurine tweets from multiple sources, filter out sent, and pick one randomly."""
print("🔎 Figurines: Fetching figurine tweets by Latest across sources")
logger.info("Fetching figurine tweets by Latest across sources")
tweets = await fetch_figurine_tweets_latest(limit_per_source=10)
if not tweets:
print("📭 No figurine tweets found across sources")
logger.warning("No figurine tweets found across sources")
return None
sent_urls = set(load_sent_tweets())
fresh = [t for t in tweets if t.get("url") not in sent_urls]
print(f"🧮 Figurines: {len(tweets)} total, {len(fresh)} fresh after filtering sent")
logger.debug(f"{len(tweets)} total, {len(fresh)} fresh after filtering sent")
if not fresh:
print(" All figurine tweets have been sent before; allowing reuse")
logger.warning("All figurine tweets have been sent before; allowing reuse")
fresh = tweets
chosen = random.choice(fresh)
print(f"🎯 Chosen figurine tweet: {chosen.get('url')}")
logger.info(f"Chosen figurine tweet: {chosen.get('url')}")
return chosen
async def send_figurine_dm_to_user(client: discord.Client, user_id: int, tweet: Dict[str, Any]) -> Tuple[bool, str]:
"""Send the figurine tweet to a single subscriber via DM, with analysis and LLM commentary."""
try:
print(f"✉️ Figurines: Preparing DM to user {user_id}")
logger.debug(f"Preparing DM to user {user_id}")
user = client.get_user(user_id)
if user is None:
# Try fetching
@@ -169,7 +171,7 @@ async def send_figurine_dm_to_user(client: discord.Client, user_id: int, tweet:
img_desc = await analyze_image_with_qwen(base64_img)
base_prompt += f"\n\nImage looks like: {img_desc}"
except Exception as e:
print(f"⚠️ Image analysis failed: {e}")
logger.warning(f"Image analysis failed: {e}")
# Include tweet text too
tweet_text = tweet.get("text", "").strip()
@@ -190,14 +192,14 @@ async def send_figurine_dm_to_user(client: discord.Client, user_id: int, tweet:
# Send the tweet URL first (convert to fxtwitter for better embeds)
fx_tweet_url = convert_to_fxtwitter(tweet_url)
tweet_message = await dm.send(fx_tweet_url)
print(f"✅ Figurines: Tweet URL sent to {user_id}: {fx_tweet_url}")
logger.info(f"Tweet URL sent to {user_id}: {fx_tweet_url}")
# Log the tweet URL message
dm_logger.log_user_message(user, tweet_message, is_bot_message=True)
# Send Miku's comment
comment_message = await dm.send(miku_comment)
print(f"✅ Figurines: Miku comment sent to {user_id}")
logger.info(f"Miku comment sent to {user_id}")
# Log the comment message
dm_logger.log_user_message(user, comment_message, is_bot_message=True)
@@ -212,27 +214,27 @@ async def send_figurine_dm_to_user(client: discord.Client, user_id: int, tweet:
# Use empty user prompt since this was initiated by Miku
globals.conversation_history.setdefault(user_id_str, []).append((tweet_context, miku_comment))
print(f"📝 Figurines: Messages logged to both DM history and conversation context for user {user_id}")
logger.debug(f"Messages logged to both DM history and conversation context for user {user_id}")
return True, "ok"
except Exception as e:
print(f"❌ Figurines: Failed DM to {user_id}: {e}")
logger.error(f"Failed DM to {user_id}: {e}")
return False, f"{e}"
async def send_figurine_dm_to_single_user(client: discord.Client, user_id: int, tweet_url: str = None) -> Dict[str, Any]:
"""Send a figurine tweet to a single user, either from search or specific URL."""
print(f"🎯 Figurines: Sending DM to single user {user_id}")
logger.info(f"Sending DM to single user {user_id}")
if tweet_url:
# Use specific tweet URL
print(f"📎 Figurines: Using specific tweet URL: {tweet_url}")
logger.info(f"Using specific tweet URL: {tweet_url}")
tweet = await fetch_specific_tweet_by_url(tweet_url)
if not tweet:
return {"status": "error", "message": "Failed to fetch specified tweet"}
else:
# Search for a random tweet
print("🔎 Figurines: Searching for random figurine tweet")
logger.info("Searching for random figurine tweet")
tweet = await choose_random_figurine_tweet()
if not tweet:
return {"status": "error", "message": "No figurine tweets found"}
@@ -256,7 +258,7 @@ async def send_figurine_dm_to_single_user(client: discord.Client, user_id: int,
"failed": [],
"tweet": {"url": tweet.get("url", ""), "username": tweet.get("username", "")}
}
print(f"✅ Figurines: Single user DM sent successfully → {result}")
logger.info(f"Single user DM sent successfully → {result}")
return result
else:
result = {
@@ -265,27 +267,27 @@ async def send_figurine_dm_to_single_user(client: discord.Client, user_id: int,
"failed": [{"user_id": str(user_id), "error": msg}],
"message": f"Failed to send DM: {msg}"
}
print(f"❌ Figurines: Single user DM failed → {result}")
logger.error(f"Single user DM failed → {result}")
return result
async def fetch_specific_tweet_by_url(tweet_url: str) -> Dict[str, Any] | None:
"""Fetch a specific tweet by URL for manual figurine notifications."""
try:
print(f"🔗 Figurines: Fetching specific tweet from URL: {tweet_url}")
logger.debug(f"Fetching specific tweet from URL: {tweet_url}")
# Extract tweet ID from URL
tweet_id = None
if "/status/" in tweet_url:
try:
tweet_id = tweet_url.split("/status/")[1].split("?")[0].split("/")[0]
print(f"📋 Figurines: Extracted tweet ID: {tweet_id}")
logger.debug(f"Extracted tweet ID: {tweet_id}")
except Exception as e:
print(f"❌ Figurines: Failed to extract tweet ID from URL: {e}")
logger.error(f"Failed to extract tweet ID from URL: {e}")
return None
if not tweet_id:
print("❌ Figurines: Could not extract tweet ID from URL")
logger.error("Could not extract tweet ID from URL")
return None
# Set up twscrape API (same pattern as existing functions)
@@ -313,15 +315,15 @@ async def fetch_specific_tweet_by_url(tweet_url: str) -> Dict[str, Any] | None:
# Try to fetch the tweet using search instead of tweet_details
# Search for the specific tweet ID should return it if accessible
print(f"🔍 Figurines: Searching for tweet with ID {tweet_id}")
logger.debug(f"Searching for tweet with ID {tweet_id}")
search_results = []
try:
# Search using the tweet ID - this should find the specific tweet
from twscrape import gather
search_results = await gather(api.search(f"{tweet_id}", limit=1))
print(f"🔍 Figurines: Search returned {len(search_results)} results")
logger.debug(f"Search returned {len(search_results)} results")
except Exception as search_error:
print(f"⚠️ Figurines: Search failed: {search_error}")
logger.warning(f"Search failed: {search_error}")
return None
# Check if we found the tweet
@@ -329,21 +331,21 @@ async def fetch_specific_tweet_by_url(tweet_url: str) -> Dict[str, Any] | None:
for tweet in search_results:
if str(tweet.id) == str(tweet_id):
tweet_data = tweet
print(f"✅ Figurines: Found matching tweet with ID {tweet.id}")
logger.debug(f"Found matching tweet with ID {tweet.id}")
break
if not tweet_data and search_results:
# If no exact match but we have results, use the first one
tweet_data = search_results[0]
print(f"🔍 Figurines: Using first search result with ID {tweet_data.id}")
logger.debug(f"Using first search result with ID {tweet_data.id}")
if tweet_data:
# Extract data using the same pattern as the working search code
username = tweet_data.user.username if hasattr(tweet_data, 'user') and tweet_data.user else "unknown"
text_content = tweet_data.rawContent if hasattr(tweet_data, 'rawContent') else ""
print(f"🔍 Figurines: Found tweet from @{username}")
print(f"🔍 Figurines: Tweet text: {text_content[:100]}...")
logger.debug(f"Found tweet from @{username}")
logger.debug(f"Tweet text: {text_content[:100]}...")
# For media, we'll need to extract it from the tweet_url using the same method as other functions
# But for now, let's see if we can get basic tweet data working first
@@ -354,37 +356,37 @@ async def fetch_specific_tweet_by_url(tweet_url: str) -> Dict[str, Any] | None:
"media": [] # We'll add media extraction later
}
print(f"✅ Figurines: Successfully fetched tweet from @{result['username']}")
logger.info(f"Successfully fetched tweet from @{result['username']}")
return result
else:
print("❌ Figurines: No tweet found with the specified ID")
logger.error("No tweet found with the specified ID")
return None
except Exception as e:
print(f"❌ Figurines: Error fetching tweet by URL: {e}")
logger.error(f"Error fetching tweet by URL: {e}")
return None
async def send_figurine_dm_to_all_subscribers(client: discord.Client, tweet_url: str = None) -> Dict[str, Any]:
"""Pick a figurine tweet and DM it to all subscribers, recording the sent URL."""
print("🚀 Figurines: Sending figurine DM to all subscribers")
logger.info("Sending figurine DM to all subscribers")
subscribers = load_subscribers()
if not subscribers:
print(" Figurines: No subscribers configured")
logger.warning("No subscribers configured")
return {"status": "no_subscribers"}
if tweet_url:
# Use specific tweet URL
print(f"📎 Figurines: Using specific tweet URL for all subscribers: {tweet_url}")
logger.info(f"Using specific tweet URL for all subscribers: {tweet_url}")
tweet = await fetch_specific_tweet_by_url(tweet_url)
if not tweet:
print(" Figurines: Failed to fetch specified tweet")
logger.warning("Failed to fetch specified tweet")
return {"status": "no_tweet", "message": "Failed to fetch specified tweet"}
else:
# Search for random tweet
tweet = await choose_random_figurine_tweet()
if tweet is None:
print(" Figurines: No tweet to send")
logger.warning("No tweet to send")
return {"status": "no_tweet"}
results = {"sent": [], "failed": []}
@@ -393,7 +395,7 @@ async def send_figurine_dm_to_all_subscribers(client: discord.Client, tweet_url:
if ok:
results["sent"].append(str(uid))
else:
print(f"⚠️ Failed to DM user {uid}: {msg}")
logger.warning(f"Failed to DM user {uid}: {msg}")
results["failed"].append({"user_id": str(uid), "error": msg})
# Record as sent if at least one success to avoid repeats
@@ -407,7 +409,7 @@ async def send_figurine_dm_to_all_subscribers(client: discord.Client, tweet_url:
save_sent_tweets(sent_urls)
summary = {"status": "ok", **results, "tweet": {"url": tweet.get("url", ""), "username": tweet.get("username", "")}}
print(f"📦 Figurines: DM send complete → {summary}")
logger.info(f"DM send complete → {summary}")
return summary

View File

@@ -14,6 +14,9 @@ import time
from typing import Optional, Tuple
import globals
from utils.llm import query_llama
from utils.logger import get_logger
logger = get_logger('media')
# Image generation detection patterns
IMAGE_REQUEST_PATTERNS = [
@@ -133,11 +136,11 @@ def find_latest_generated_image(prompt_id: str, expected_filename: str = None) -
recent_threshold = time.time() - 600 # 10 minutes
for file_path in all_files:
if os.path.getmtime(file_path) > recent_threshold:
print(f"🎨 Found recent image: {file_path}")
logger.debug(f"Found recent image: {file_path}")
return file_path
except Exception as e:
print(f"⚠️ Error searching in {output_dir}: {e}")
logger.error(f"Error searching in {output_dir}: {e}")
continue
return None
@@ -156,7 +159,7 @@ async def generate_image_with_comfyui(prompt: str) -> Optional[str]:
# Load the workflow template
workflow_path = "Miku_BasicWorkflow.json"
if not os.path.exists(workflow_path):
print(f"Workflow template not found: {workflow_path}")
logger.error(f"Workflow template not found: {workflow_path}")
return None
with open(workflow_path, 'r') as f:
@@ -186,29 +189,29 @@ async def generate_image_with_comfyui(prompt: str) -> Optional[str]:
async with test_session.get(f"{url}/system_stats", timeout=timeout) as test_response:
if test_response.status == 200:
comfyui_url = url
print(f"ComfyUI found at: {url}")
logger.debug(f"ComfyUI found at: {url}")
break
except:
continue
if not comfyui_url:
print(f"ComfyUI not reachable at any of: {comfyui_urls}")
logger.error(f"ComfyUI not reachable at any of: {comfyui_urls}")
return None
async with aiohttp.ClientSession() as session:
# Submit the generation request
async with session.post(f"{comfyui_url}/prompt", json=payload) as response:
if response.status != 200:
print(f"ComfyUI request failed: {response.status}")
logger.error(f"ComfyUI request failed: {response.status}")
return None
result = await response.json()
prompt_id = result.get("prompt_id")
if not prompt_id:
print("No prompt_id received from ComfyUI")
logger.error("No prompt_id received from ComfyUI")
return None
print(f"🎨 ComfyUI generation started with prompt_id: {prompt_id}")
logger.info(f"ComfyUI generation started with prompt_id: {prompt_id}")
# Poll for completion (timeout after 5 minutes)
timeout = 300 # 5 minutes
@@ -242,20 +245,20 @@ async def generate_image_with_comfyui(prompt: str) -> Optional[str]:
# Verify the file exists before returning
if os.path.exists(image_path):
print(f"Image generated successfully: {image_path}")
logger.info(f"Image generated successfully: {image_path}")
return image_path
else:
# Try alternative paths in case of different mounting
alt_path = os.path.join("/app/ComfyUI/output", filename)
if os.path.exists(alt_path):
print(f"Image generated successfully: {alt_path}")
logger.info(f"Image generated successfully: {alt_path}")
return alt_path
else:
print(f"⚠️ Generated image not found at expected paths: {image_path} or {alt_path}")
logger.warning(f"Generated image not found at expected paths: {image_path} or {alt_path}")
continue
# If we couldn't find the image via API, try the fallback method
print("🔍 Image not found via API, trying fallback method...")
logger.debug("Image not found via API, trying fallback method...")
fallback_image = find_latest_generated_image(prompt_id)
if fallback_image:
return fallback_image
@@ -263,19 +266,19 @@ async def generate_image_with_comfyui(prompt: str) -> Optional[str]:
# Wait before polling again
await asyncio.sleep(2)
print("ComfyUI generation timed out")
logger.error("ComfyUI generation timed out")
# Final fallback: look for the most recent image
print("🔍 Trying final fallback: most recent image...")
logger.debug("Trying final fallback: most recent image...")
fallback_image = find_latest_generated_image(prompt_id)
if fallback_image:
print(f"Found image via fallback method: {fallback_image}")
logger.info(f"Found image via fallback method: {fallback_image}")
return fallback_image
return None
except Exception as e:
print(f"Error in generate_image_with_comfyui: {e}")
logger.error(f"Error in generate_image_with_comfyui: {e}")
return None
async def handle_image_generation_request(message, prompt: str) -> bool:
@@ -307,7 +310,7 @@ async def handle_image_generation_request(message, prompt: str) -> bool:
# Start typing to show we're working
async with message.channel.typing():
# Generate the image
print(f"🎨 Starting image generation for prompt: {prompt}")
logger.info(f"Starting image generation for prompt: {prompt}")
image_path = await generate_image_with_comfyui(prompt)
if image_path and os.path.exists(image_path):
@@ -322,7 +325,7 @@ async def handle_image_generation_request(message, prompt: str) -> bool:
await message.channel.send(completion_response, file=file)
print(f"Image sent successfully to {message.author.display_name}")
logger.info(f"Image sent successfully to {message.author.display_name}")
# Log to DM history if it's a DM
if is_dm:
@@ -336,11 +339,11 @@ async def handle_image_generation_request(message, prompt: str) -> bool:
error_response = await query_llama(error_prompt, user_id=user_id, guild_id=guild_id, response_type=response_type)
await message.channel.send(error_response)
print(f"Image generation failed for prompt: {prompt}")
logger.error(f"Image generation failed for prompt: {prompt}")
return False
except Exception as e:
print(f"Error in handle_image_generation_request: {e}")
logger.error(f"Error in handle_image_generation_request: {e}")
# Send error message
try:

View File

@@ -10,6 +10,10 @@ from PIL import Image
import re
import globals
from utils.logger import get_logger
logger = get_logger('vision')
# No need for switch_model anymore - llama-swap handles this automatically
@@ -47,7 +51,7 @@ async def extract_tenor_gif_url(tenor_url):
match = re.search(r'tenor\.com/(\d+)\.gif', tenor_url)
if not match:
print(f"⚠️ Could not extract Tenor GIF ID from: {tenor_url}")
logger.warning(f"Could not extract Tenor GIF ID from: {tenor_url}")
return None
gif_id = match.group(1)
@@ -60,7 +64,7 @@ async def extract_tenor_gif_url(tenor_url):
async with aiohttp.ClientSession() as session:
async with session.head(media_url) as resp:
if resp.status == 200:
print(f"Found Tenor GIF: {media_url}")
logger.debug(f"Found Tenor GIF: {media_url}")
return media_url
# If that didn't work, try alternative formats
@@ -69,14 +73,14 @@ async def extract_tenor_gif_url(tenor_url):
async with aiohttp.ClientSession() as session:
async with session.head(alt_url) as resp:
if resp.status == 200:
print(f"Found Tenor GIF (alternative): {alt_url}")
logger.debug(f"Found Tenor GIF (alternative): {alt_url}")
return alt_url
print(f"⚠️ Could not find working Tenor media URL for ID: {gif_id}")
logger.warning(f"Could not find working Tenor media URL for ID: {gif_id}")
return None
except Exception as e:
print(f"⚠️ Error extracting Tenor GIF URL: {e}")
logger.error(f"Error extracting Tenor GIF URL: {e}")
return None
@@ -114,7 +118,7 @@ async def convert_gif_to_mp4(gif_bytes):
with open(temp_mp4_path, 'rb') as f:
mp4_bytes = f.read()
print(f"Converted GIF to MP4 ({len(gif_bytes)} bytes → {len(mp4_bytes)} bytes)")
logger.info(f"Converted GIF to MP4 ({len(gif_bytes)} bytes → {len(mp4_bytes)} bytes)")
return mp4_bytes
finally:
@@ -125,10 +129,10 @@ async def convert_gif_to_mp4(gif_bytes):
os.remove(temp_mp4_path)
except subprocess.CalledProcessError as e:
print(f"⚠️ ffmpeg error converting GIF to MP4: {e.stderr.decode()}")
logger.error(f"ffmpeg error converting GIF to MP4: {e.stderr.decode()}")
return None
except Exception as e:
print(f"⚠️ Error converting GIF to MP4: {e}")
logger.error(f"Error converting GIF to MP4: {e}")
import traceback
traceback.print_exc()
return None
@@ -165,7 +169,7 @@ async def extract_video_frames(video_bytes, num_frames=4):
if frames:
return frames
except Exception as e:
print(f"Not a GIF, trying video extraction: {e}")
logger.debug(f"Not a GIF, trying video extraction: {e}")
# For video files (MP4, WebM, etc.), use ffmpeg
import subprocess
@@ -222,7 +226,7 @@ async def extract_video_frames(video_bytes, num_frames=4):
os.remove(temp_video_path)
except Exception as e:
print(f"⚠️ Error extracting frames: {e}")
logger.error(f"Error extracting frames: {e}")
import traceback
traceback.print_exc()
@@ -271,10 +275,10 @@ async def analyze_image_with_vision(base64_img):
return data.get("choices", [{}])[0].get("message", {}).get("content", "No description.")
else:
error_text = await response.text()
print(f"Vision API error: {response.status} - {error_text}")
logger.error(f"Vision API error: {response.status} - {error_text}")
return f"Error analyzing image: {response.status}"
except Exception as e:
print(f"⚠️ Error in analyze_image_with_vision: {e}")
logger.error(f"Error in analyze_image_with_vision: {e}")
return f"Error analyzing image: {str(e)}"
@@ -333,10 +337,10 @@ async def analyze_video_with_vision(video_frames, media_type="video"):
return data.get("choices", [{}])[0].get("message", {}).get("content", "No description.")
else:
error_text = await response.text()
print(f"Vision API error: {response.status} - {error_text}")
logger.error(f"Vision API error: {response.status} - {error_text}")
return f"Error analyzing video: {response.status}"
except Exception as e:
print(f"⚠️ Error in analyze_video_with_vision: {e}")
logger.error(f"Error in analyze_video_with_vision: {e}")
return f"Error analyzing video: {str(e)}"

View File

@@ -3,6 +3,9 @@
import random
import globals
from utils.llm import query_llama # Adjust path as needed
from utils.logger import get_logger
logger = get_logger('bot')
async def detect_and_react_to_kindness(message, after_reply=False, server_context=None):
@@ -19,14 +22,14 @@ async def detect_and_react_to_kindness(message, after_reply=False, server_contex
await message.add_reaction(emoji)
globals.kindness_reacted_messages.add(message.id)
message.kindness_reacted = True # Mark as done
print("Kindness detected via keywords. Reacted immediately.")
logger.info("Kindness detected via keywords. Reacted immediately.")
except Exception as e:
print(f"⚠️ Error adding reaction: {e}")
logger.error(f"Error adding reaction: {e}")
return
# 2. If not after_reply, defer model-based check
if not after_reply:
print("🗝️ No kindness via keywords. Deferring...")
logger.debug("No kindness via keywords. Deferring...")
return
# 3. Model-based detection
@@ -42,8 +45,8 @@ async def detect_and_react_to_kindness(message, after_reply=False, server_contex
if result.strip().lower().startswith("yes"):
await message.add_reaction(emoji)
globals.kindness_reacted_messages.add(message.id)
print("Kindness detected via model. Reacted.")
logger.info("Kindness detected via model. Reacted.")
else:
print("🧊 No kindness detected.")
logger.debug("No kindness detected.")
except Exception as e:
print(f"⚠️ Error during kindness analysis: {e}")
logger.error(f"Error during kindness analysis: {e}")

View File

@@ -10,6 +10,10 @@ import os
from utils.context_manager import get_context_for_response_type, get_complete_context
from utils.moods import load_mood_description
from utils.conversation_history import conversation_history
from utils.logger import get_logger
logger = get_logger('llm')
def get_current_gpu_url():
"""Get the URL for the currently selected GPU for text models"""
@@ -23,7 +27,7 @@ def get_current_gpu_url():
else:
return globals.LLAMA_URL
except Exception as e:
print(f"⚠️ GPU state read error: {e}, defaulting to NVIDIA")
logger.warning(f"GPU state read error: {e}, defaulting to NVIDIA")
# Default to NVIDIA if state file doesn't exist
return globals.LLAMA_URL
@@ -102,7 +106,7 @@ async def query_llama(user_prompt, user_id, guild_id=None, response_type="dm_res
if model is None:
if evil_mode:
model = globals.EVIL_TEXT_MODEL # Use DarkIdol uncensored model
print(f"😈 Using evil model: {model}")
logger.info(f"Using evil model: {model}")
else:
model = globals.TEXT_MODEL
@@ -155,7 +159,7 @@ You ARE Miku. Act like it."""
is_sleeping = False
forced_angry_until = None
just_woken_up = False
print(f"😈 Using Evil mode with mood: {current_mood_name}")
logger.info(f"Using Evil mode with mood: {current_mood_name}")
else:
current_mood = globals.DM_MOOD_DESCRIPTION # Default to DM mood
current_mood_name = globals.DM_MOOD # Default to DM mood name
@@ -175,14 +179,14 @@ You ARE Miku. Act like it."""
is_sleeping = server_config.is_sleeping
forced_angry_until = server_config.forced_angry_until
just_woken_up = server_config.just_woken_up
print(f"🎭 Using server mood: {current_mood_name} for guild {guild_id}")
logger.debug(f"Using server mood: {current_mood_name} for guild {guild_id}")
else:
print(f"⚠️ No server config found for guild {guild_id}, using DM mood")
logger.warning(f"No server config found for guild {guild_id}, using DM mood")
except Exception as e:
print(f"⚠️ Failed to get server mood for guild {guild_id}, falling back to DM mood: {e}")
logger.error(f"Failed to get server mood for guild {guild_id}, falling back to DM mood: {e}")
# Fall back to DM mood if server mood fails
elif not evil_mode:
print(f"🌍 Using DM mood: {globals.DM_MOOD}")
logger.debug(f"Using DM mood: {globals.DM_MOOD}")
# Append angry wake-up note if JUST_WOKEN_UP flag is set (only in non-evil mode)
if just_woken_up and not evil_mode:
@@ -262,7 +266,7 @@ Please respond in a way that reflects this emotional tone.{pfp_context}"""
try:
# Get current GPU URL based on user selection
llama_url = get_current_gpu_url()
print(f"🎮 Using GPU endpoint: {llama_url}")
logger.debug(f"Using GPU endpoint: {llama_url}")
# Add timeout to prevent hanging indefinitely
timeout = aiohttp.ClientTimeout(total=300) # 300 second timeout
@@ -301,13 +305,13 @@ Please respond in a way that reflects this emotional tone.{pfp_context}"""
return reply
else:
error_text = await response.text()
print(f"Error from llama-swap: {response.status} - {error_text}")
logger.error(f"Error from llama-swap: {response.status} - {error_text}")
# Don't save error responses to conversation history
return f"Error: {response.status}"
except asyncio.TimeoutError:
return "Sorry, the response took too long. Please try again."
except Exception as e:
print(f"⚠️ Error in query_llama: {e}")
logger.error(f"Error in query_llama: {e}")
return f"Sorry, there was an error: {str(e)}"
# Backward compatibility alias for existing code

286
bot/utils/log_config.py Normal file
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
import subprocess
from utils.logger import get_logger
logger = get_logger('media')
async def overlay_username_with_ffmpeg(base_video_path, output_path, username):
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
@@ -65,6 +68,6 @@ async def overlay_username_with_ffmpeg(base_video_path, output_path, username):
try:
subprocess.run(ffmpeg_command, check=True)
print("Video processed successfully with username overlays.")
logger.info("Video processed successfully with username overlays.")
except subprocess.CalledProcessError as e:
print(f"⚠️ FFmpeg error: {e}")
logger.error(f"FFmpeg error: {e}")

View File

@@ -7,6 +7,9 @@ import asyncio
from discord.ext import tasks
import globals
import datetime
from utils.logger import get_logger
logger = get_logger('mood')
MOOD_EMOJIS = {
"asleep": "💤",
@@ -47,7 +50,7 @@ def load_mood_description(mood_name: str) -> str:
with open(path, "r", encoding="utf-8") as f:
return f.read().strip()
except FileNotFoundError:
print(f"⚠️ Mood file '{mood_name}' not found. Falling back to default.")
logger.warning(f"Mood file '{mood_name}' not found. Falling back to default.")
# Return a default mood description instead of recursive call
return "I'm feeling neutral and balanced today."
@@ -120,17 +123,17 @@ def detect_mood_shift(response_text, server_context=None):
# For server context, check against server's current mood
current_mood = server_context.get('current_mood_name', 'neutral')
if current_mood != "sleepy":
print(f"Mood 'asleep' skipped - server mood isn't 'sleepy', it's '{current_mood}'")
logger.debug(f"Mood 'asleep' skipped - server mood isn't 'sleepy', it's '{current_mood}'")
continue
else:
# For DM context, check against DM mood
if globals.DM_MOOD != "sleepy":
print(f"Mood 'asleep' skipped - DM mood isn't 'sleepy', it's '{globals.DM_MOOD}'")
logger.debug(f"Mood 'asleep' skipped - DM mood isn't 'sleepy', it's '{globals.DM_MOOD}'")
continue
for phrase in phrases:
if phrase.lower() in response_text.lower():
print(f"*️⃣ Mood keyword triggered: {phrase}")
logger.info(f"Mood keyword triggered: {phrase}")
return mood
return None
@@ -155,13 +158,13 @@ async def rotate_dm_mood():
globals.DM_MOOD = new_mood
globals.DM_MOOD_DESCRIPTION = load_mood_description(new_mood)
print(f"🔄 DM mood rotated from {old_mood} to {new_mood}")
logger.info(f"DM mood rotated from {old_mood} to {new_mood}")
# Note: We don't update server nicknames here because servers have their own independent moods.
# DM mood only affects direct messages to users.
except Exception as e:
print(f"Exception in rotate_dm_mood: {e}")
logger.error(f"Exception in rotate_dm_mood: {e}")
async def update_all_server_nicknames():
"""
@@ -171,8 +174,8 @@ async def update_all_server_nicknames():
This function incorrectly used DM mood to update all server nicknames,
breaking the independent per-server mood system.
"""
print("⚠️ WARNING: update_all_server_nicknames() is deprecated and should not be called!")
print("⚠️ Use update_server_nickname(guild_id) for per-server nickname updates instead.")
logger.warning("WARNING: update_all_server_nicknames() is deprecated and should not be called!")
logger.warning("Use update_server_nickname(guild_id) for per-server nickname updates instead.")
# Do nothing - this function should not modify nicknames
async def nickname_mood_emoji(guild_id: int):
@@ -182,11 +185,11 @@ async def nickname_mood_emoji(guild_id: int):
async def update_server_nickname(guild_id: int):
"""Update nickname for a specific server based on its mood"""
try:
print(f"🎭 Starting nickname update for server {guild_id}")
logger.debug(f"Starting nickname update for server {guild_id}")
# Check if bot is ready
if not globals.client.is_ready():
print(f"⚠️ Bot not ready yet, deferring nickname update for server {guild_id}")
logger.warning(f"Bot not ready yet, deferring nickname update for server {guild_id}")
return
# Check if evil mode is active
@@ -196,7 +199,7 @@ async def update_server_nickname(guild_id: int):
from server_manager import server_manager
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No server config found for guild {guild_id}")
logger.warning(f"No server config found for guild {guild_id}")
return
if evil_mode:
@@ -209,29 +212,29 @@ async def update_server_nickname(guild_id: int):
emoji = MOOD_EMOJIS.get(mood, "")
base_name = "Hatsune Miku"
print(f"🔍 Server {guild_id} mood is: {mood} (evil_mode={evil_mode})")
print(f"🔍 Using emoji: {emoji}")
logger.debug(f"Server {guild_id} mood is: {mood} (evil_mode={evil_mode})")
logger.debug(f"Using emoji: {emoji}")
nickname = f"{base_name}{emoji}"
print(f"🔍 New nickname will be: {nickname}")
logger.debug(f"New nickname will be: {nickname}")
guild = globals.client.get_guild(guild_id)
if guild:
print(f"🔍 Found guild: {guild.name}")
logger.debug(f"Found guild: {guild.name}")
me = guild.get_member(globals.BOT_USER.id)
if me is not None:
print(f"🔍 Found bot member: {me.display_name}")
logger.debug(f"Found bot member: {me.display_name}")
try:
await me.edit(nick=nickname)
print(f"💱 Changed nickname to {nickname} in server {guild.name}")
logger.info(f"Changed nickname to {nickname} in server {guild.name}")
except Exception as e:
print(f"⚠️ Failed to update nickname in server {guild.name}: {e}")
logger.warning(f"Failed to update nickname in server {guild.name}: {e}")
else:
print(f"⚠️ Could not find bot member in server {guild.name}")
logger.warning(f"Could not find bot member in server {guild.name}")
else:
print(f"⚠️ Could not find guild {guild_id}")
logger.warning(f"Could not find guild {guild_id}")
except Exception as e:
print(f"⚠️ Error updating server nickname for guild {guild_id}: {e}")
logger.error(f"Error updating server nickname for guild {guild_id}: {e}")
import traceback
traceback.print_exc()
@@ -268,7 +271,7 @@ async def rotate_server_mood(guild_id: int):
# Block transition to asleep unless coming from sleepy
if new_mood_name == "asleep" and old_mood_name != "sleepy":
print(f"Cannot rotate to asleep from {old_mood_name}, must be sleepy first")
logger.warning(f"Cannot rotate to asleep from {old_mood_name}, must be sleepy first")
# Try to get a different mood
attempts = 0
while (new_mood_name == "asleep" or new_mood_name == old_mood_name) and attempts < 5:
@@ -282,7 +285,7 @@ async def rotate_server_mood(guild_id: int):
from utils.autonomous import on_mood_change
on_mood_change(guild_id, new_mood_name)
except Exception as mood_notify_error:
print(f"⚠️ Failed to notify autonomous engine of mood change: {mood_notify_error}")
logger.error(f"Failed to notify autonomous engine of mood change: {mood_notify_error}")
# If transitioning to asleep, set up auto-wake
if new_mood_name == "asleep":
@@ -298,22 +301,22 @@ async def rotate_server_mood(guild_id: int):
from utils.autonomous import on_mood_change
on_mood_change(guild_id, "neutral")
except Exception as mood_notify_error:
print(f"⚠️ Failed to notify autonomous engine of wake-up mood change: {mood_notify_error}")
logger.error(f"Failed to notify autonomous engine of wake-up mood change: {mood_notify_error}")
await update_server_nickname(guild_id)
print(f"🌅 Server {guild_id} woke up from auto-sleep (mood rotation)")
logger.info(f"Server {guild_id} woke up from auto-sleep (mood rotation)")
globals.client.loop.create_task(delayed_wakeup())
print(f"Scheduled auto-wake for server {guild_id} in 1 hour")
logger.info(f"Scheduled auto-wake for server {guild_id} in 1 hour")
# Update nickname for this specific server
await update_server_nickname(guild_id)
print(f"🔄 Rotated mood for server {guild_id} from {old_mood_name} to {new_mood_name}")
logger.info(f"Rotated mood for server {guild_id} from {old_mood_name} to {new_mood_name}")
except Exception as e:
print(f"Exception in rotate_server_mood for server {guild_id}: {e}")
logger.error(f"Exception in rotate_server_mood for server {guild_id}: {e}")
async def clear_angry_mood_after_delay():
"""Clear angry mood after delay (legacy function - now handled per-server)"""
print("⚠️ clear_angry_mood_after_delay called - this function is deprecated")
logger.warning("clear_angry_mood_after_delay called - this function is deprecated")
pass

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
"""
import discord
import asyncio
import time
import globals
from utils.logger import get_logger
logger = get_logger('persona')
"""
import os
import json
import time
@@ -38,7 +47,7 @@ ARGUMENT_TENSION_THRESHOLD = 0.75 # Tension level that triggers argument escal
# Initial trigger settings
INTERJECTION_COOLDOWN_HARD = 180 # 3 minutes hard block
INTERJECTION_COOLDOWN_SOFT = 900 # 15 minutes for full recovery
INTERJECTION_THRESHOLD = 0.75 # Score needed to trigger interjection (lowered to account for mood multipliers)
INTERJECTION_THRESHOLD = 0.5 # Score needed to trigger interjection
# ============================================================================
# INTERJECTION SCORER (Initial Trigger Decision)
@@ -62,15 +71,15 @@ class InterjectionScorer:
def sentiment_analyzer(self):
"""Lazy load sentiment analyzer"""
if self._sentiment_analyzer is None:
print("🔄 Loading sentiment analyzer for persona dialogue...")
logger.debug("Loading sentiment analyzer for persona dialogue...")
try:
self._sentiment_analyzer = pipeline(
"sentiment-analysis",
model="distilbert-base-uncased-finetuned-sst-2-english"
)
print("Sentiment analyzer loaded")
logger.info("Sentiment analyzer loaded")
except Exception as e:
print(f"⚠️ Failed to load sentiment analyzer: {e}")
logger.error(f"Failed to load sentiment analyzer: {e}")
self._sentiment_analyzer = None
return self._sentiment_analyzer
@@ -97,8 +106,8 @@ class InterjectionScorer:
opposite_persona = "evil" if current_persona == "miku" else "miku"
print(f"🔍 [Interjection] Analyzing content: '{message.content[:100]}...'")
print(f"🔍 [Interjection] Current persona: {current_persona}, Opposite: {opposite_persona}")
logger.debug(f"[Interjection] Analyzing content: '{message.content[:100]}...'")
logger.debug(f"[Interjection] Current persona: {current_persona}, Opposite: {opposite_persona}")
# Calculate score from various factors
score = 0.0
@@ -106,7 +115,7 @@ class InterjectionScorer:
# Factor 1: Direct addressing (automatic trigger)
if self._mentions_opposite(message.content, opposite_persona):
print(f"[Interjection] Direct mention of {opposite_persona} detected!")
logger.info(f"[Interjection] Direct mention of {opposite_persona} detected!")
return True, "directly_addressed", 1.0
# Factor 2: Topic relevance
@@ -147,8 +156,8 @@ class InterjectionScorer:
reason_str = " | ".join(reasons) if reasons else "no_triggers"
if should_interject:
print(f"{opposite_persona.upper()} WILL INTERJECT (score: {score:.2f})")
print(f" Reasons: {reason_str}")
logger.info(f"{opposite_persona.upper()} WILL INTERJECT (score: {score:.2f})")
logger.info(f" Reasons: {reason_str}")
return should_interject, reason_str, score
@@ -156,12 +165,12 @@ class InterjectionScorer:
"""Fast rejection criteria"""
# System messages
if message.type != discord.MessageType.default:
print(f"[Basic Filter] System message type: {message.type}")
logger.debug(f"[Basic Filter] System message type: {message.type}")
return False
# Bipolar mode must be enabled
if not globals.BIPOLAR_MODE:
print(f"[Basic Filter] Bipolar mode not enabled")
logger.debug(f"[Basic Filter] Bipolar mode not enabled")
return False
# Allow bot's own messages (we're checking them for interjections!)
@@ -170,10 +179,10 @@ class InterjectionScorer:
if message.author.bot and not message.webhook_id:
# Check if it's our own bot
if message.author.id != globals.client.user.id:
print(f"[Basic Filter] Other bot message (not our bot)")
logger.debug(f"[Basic Filter] Other bot message (not our bot)")
return False
print(f"[Basic Filter] Passed (bot={message.author.bot}, webhook={message.webhook_id}, our_bot={message.author.id == globals.client.user.id if message.author.bot else 'N/A'})")
logger.debug(f"[Basic Filter] Passed (bot={message.author.bot}, webhook={message.webhook_id}, our_bot={message.author.id == globals.client.user.id if message.author.bot else 'N/A'})")
return True
def _mentions_opposite(self, content: str, opposite_persona: str) -> bool:
@@ -233,7 +242,7 @@ class InterjectionScorer:
return min(confidence * 0.6 + intensity_markers, 1.0)
except Exception as e:
print(f"⚠️ Sentiment analysis error: {e}")
logger.error(f"Sentiment analysis error: {e}")
return 0.5
def _detect_personality_clash(self, content: str, opposite_persona: str) -> float:
@@ -364,15 +373,15 @@ class PersonaDialogue:
}
self.active_dialogues[channel_id] = state
globals.LAST_PERSONA_DIALOGUE_TIME = time.time()
print(f"💬 Started persona dialogue in channel {channel_id}")
logger.info(f"Started persona dialogue in channel {channel_id}")
return state
def end_dialogue(self, channel_id: int):
"""End a dialogue in a channel"""
if channel_id in self.active_dialogues:
state = self.active_dialogues[channel_id]
print(f"🏁 Ended persona dialogue in channel {channel_id}")
print(f" Turns: {state['turn_count']}, Final tension: {state['tension']:.2f}")
logger.info(f"Ended persona dialogue in channel {channel_id}")
logger.info(f" Turns: {state['turn_count']}, Final tension: {state['tension']:.2f}")
del self.active_dialogues[channel_id]
# ========================================================================
@@ -400,7 +409,7 @@ class PersonaDialogue:
else:
base_delta = -sentiment_score * 0.05
except Exception as e:
print(f"⚠️ Sentiment analysis error in tension calc: {e}")
logger.error(f"Sentiment analysis error in tension calc: {e}")
text_lower = response_text.lower()
@@ -557,7 +566,7 @@ On a new line after your response, write:
# Override: If the response contains a question mark, always continue
if '?' in response_text:
print(f"⚠️ [Parse Override] Question detected, forcing continue=YES")
logger.debug(f"[Parse Override] Question detected, forcing continue=YES")
should_continue = True
if confidence == "LOW":
confidence = "MEDIUM"
@@ -605,12 +614,12 @@ You can use emojis naturally! ✨💙"""
# Safety limits
if state["turn_count"] >= MAX_TURNS:
print(f"🛑 Dialogue reached {MAX_TURNS} turns, ending")
logger.info(f"Dialogue reached {MAX_TURNS} turns, ending")
self.end_dialogue(channel_id)
return
if time.time() - state["started_at"] > DIALOGUE_TIMEOUT:
print(f"🛑 Dialogue timeout (15 min), ending")
logger.info(f"Dialogue timeout (15 min), ending")
self.end_dialogue(channel_id)
return
@@ -625,7 +634,7 @@ You can use emojis naturally! ✨💙"""
)
if not response_text:
print(f"⚠️ Failed to generate response for {responding_persona}")
logger.error(f"Failed to generate response for {responding_persona}")
self.end_dialogue(channel_id)
return
@@ -639,11 +648,11 @@ You can use emojis naturally! ✨💙"""
"total": state["tension"],
})
print(f"🌡️ Tension: {state['tension']:.2f} (delta: {tension_delta:+.2f})")
logger.debug(f"Tension: {state['tension']:.2f} (delta: {tension_delta:+.2f})")
# Check if we should escalate to argument
if state["tension"] >= ARGUMENT_TENSION_THRESHOLD:
print(f"🔥 TENSION THRESHOLD REACHED ({state['tension']:.2f}) - ESCALATING TO ARGUMENT")
logger.info(f"TENSION THRESHOLD REACHED ({state['tension']:.2f}) - ESCALATING TO ARGUMENT")
# Send the response that pushed us over
await self._send_as_persona(channel, responding_persona, response_text)
@@ -659,7 +668,7 @@ You can use emojis naturally! ✨💙"""
state["turn_count"] += 1
state["last_speaker"] = responding_persona
print(f"🗣️ Turn {state['turn_count']}: {responding_persona} | Continue: {should_continue} ({confidence}) | Tension: {state['tension']:.2f}")
logger.debug(f"Turn {state['turn_count']}: {responding_persona} | Continue: {should_continue} ({confidence}) | Tension: {state['tension']:.2f}")
# Decide what happens next
opposite = "evil" if responding_persona == "miku" else "miku"
@@ -677,14 +686,14 @@ You can use emojis naturally! ✨💙"""
)
else:
# Clear signal to end
print(f"🏁 Dialogue ended naturally after {state['turn_count']} turns (tension: {state['tension']:.2f})")
logger.info(f"Dialogue ended naturally after {state['turn_count']} turns (tension: {state['tension']:.2f})")
self.end_dialogue(channel_id)
async def _next_turn(self, channel: discord.TextChannel, persona: str):
"""Queue the next turn"""
# Check if dialogue was interrupted
if await self._was_interrupted(channel):
print(f"💬 Dialogue interrupted by other activity")
logger.info(f"Dialogue interrupted by other activity")
self.end_dialogue(channel.id)
return
@@ -741,7 +750,7 @@ Don't force a response if you have nothing meaningful to contribute."""
return
if "[DONE]" in response.upper():
print(f"🏁 {persona} chose not to respond, dialogue ended (tension: {state['tension']:.2f})")
logger.info(f"{persona} chose not to respond, dialogue ended (tension: {state['tension']:.2f})")
self.end_dialogue(channel_id)
else:
clean_response = response.replace("[DONE]", "").strip()
@@ -750,11 +759,11 @@ Don't force a response if you have nothing meaningful to contribute."""
tension_delta = self.calculate_tension_delta(clean_response, state["tension"])
state["tension"] = max(0.0, min(1.0, state["tension"] + tension_delta))
print(f"🌡️ Last word tension: {state['tension']:.2f} (delta: {tension_delta:+.2f})")
logger.debug(f"Last word tension: {state['tension']:.2f} (delta: {tension_delta:+.2f})")
# Check for argument escalation
if state["tension"] >= ARGUMENT_TENSION_THRESHOLD:
print(f"🔥 TENSION THRESHOLD REACHED on last word - ESCALATING TO ARGUMENT")
logger.info(f"TENSION THRESHOLD REACHED on last word - ESCALATING TO ARGUMENT")
await self._send_as_persona(channel, persona, clean_response)
await self._escalate_to_argument(channel, persona, clean_response)
return
@@ -782,7 +791,7 @@ Don't force a response if you have nothing meaningful to contribute."""
]
if all(closing_indicators):
print(f"🏁 Dialogue ended after last word, {state['turn_count']} turns total")
logger.info(f"Dialogue ended after last word, {state['turn_count']} turns total")
self.end_dialogue(channel.id)
else:
asyncio.create_task(self._next_turn(channel, opposite))
@@ -802,7 +811,7 @@ Don't force a response if you have nothing meaningful to contribute."""
# Don't start if an argument is already going
if is_argument_in_progress(channel.id):
print(f"⚠️ Argument already in progress, skipping escalation")
logger.warning(f"Argument already in progress, skipping escalation")
return
# Build context for the argument
@@ -811,7 +820,7 @@ The last thing said was: "{triggering_message}"
This pushed things over the edge into a full argument."""
print(f"⚔️ Escalating to argument in #{channel.name}")
logger.info(f"Escalating to argument in #{channel.name}")
# Use the existing argument system
# Pass the triggering message so the opposite persona responds to it
@@ -839,7 +848,7 @@ This pushed things over the edge into a full argument."""
if msg.author.id != globals.client.user.id:
return True
except Exception as e:
print(f"⚠️ Error checking for interruption: {e}")
logger.warning(f"Error checking for interruption: {e}")
return False
@@ -853,7 +862,7 @@ This pushed things over the edge into a full argument."""
messages.reverse()
except Exception as e:
print(f"⚠️ Error building conversation context: {e}")
logger.warning(f"Error building conversation context: {e}")
return '\n'.join(messages)
@@ -881,7 +890,7 @@ This pushed things over the edge into a full argument."""
webhooks = await get_or_create_webhooks_for_channel(channel)
if not webhooks:
print(f"⚠️ Could not get webhooks for #{channel.name}")
logger.warning(f"Could not get webhooks for #{channel.name}")
return
webhook = webhooks["evil_miku"] if persona == "evil" else webhooks["miku"]
@@ -890,7 +899,7 @@ This pushed things over the edge into a full argument."""
try:
await webhook.send(content=content, username=display_name)
except Exception as e:
print(f"⚠️ Error sending as {persona}: {e}")
logger.error(f"Error sending as {persona}: {e}")
# ============================================================================
@@ -929,24 +938,24 @@ async def check_for_interjection(message: discord.Message, current_persona: str)
Returns:
True if an interjection was triggered, False otherwise
"""
print(f"🔍 [Persona Dialogue] Checking interjection for message from {current_persona}")
logger.debug(f"[Persona Dialogue] Checking interjection for message from {current_persona}")
scorer = get_interjection_scorer()
dialogue_manager = get_dialogue_manager()
# Don't trigger if dialogue already active
if dialogue_manager.is_dialogue_active(message.channel.id):
print(f"⏸️ [Persona Dialogue] Dialogue already active in channel {message.channel.id}")
logger.debug(f"[Persona Dialogue] Dialogue already active in channel {message.channel.id}")
return False
# Check if we should interject
should_interject, reason, score = await scorer.should_interject(message, current_persona)
print(f"📊 [Persona Dialogue] Interjection check: should_interject={should_interject}, reason={reason}, score={score:.2f}")
logger.debug(f"[Persona Dialogue] Interjection check: should_interject={should_interject}, reason={reason}, score={score:.2f}")
if should_interject:
opposite_persona = "evil" if current_persona == "miku" else "miku"
print(f"🎭 Triggering {opposite_persona} interjection (reason: {reason}, score: {score:.2f})")
logger.info(f"Triggering {opposite_persona} interjection (reason: {reason}, score: {score:.2f})")
# Start dialogue with the opposite persona responding first
dialogue_manager.start_dialogue(message.channel.id)

View File

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

View File

@@ -13,6 +13,9 @@ import globals
from server_manager import server_manager
from utils.llm import query_llama
from utils.dm_interaction_analyzer import dm_analyzer
from utils.logger import get_logger
logger = get_logger('scheduled')
BEDTIME_TRACKING_FILE = "last_bedtime_targets.json"
@@ -20,7 +23,7 @@ async def send_monday_video_for_server(guild_id: int):
"""Send Monday video for a specific server"""
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
# No need to switch model - llama-swap handles this automatically
@@ -37,7 +40,7 @@ async def send_monday_video_for_server(guild_id: int):
for channel_id in target_channel_ids:
channel = globals.client.get_channel(channel_id)
if channel is None:
print(f"Could not find channel with ID {channel_id} in server {server_config.guild_name}")
logger.error(f"Could not find channel with ID {channel_id} in server {server_config.guild_name}")
continue
try:
@@ -45,9 +48,9 @@ async def send_monday_video_for_server(guild_id: int):
# Send video link
await channel.send(f"[Happy Miku Monday!]({video_url})")
print(f"Sent Monday video to channel ID {channel_id} in server {server_config.guild_name}")
logger.info(f"Sent Monday video to channel ID {channel_id} in server {server_config.guild_name}")
except Exception as e:
print(f"⚠️ Failed to send video to channel ID {channel_id} in server {server_config.guild_name}: {e}")
logger.error(f"Failed to send video to channel ID {channel_id} in server {server_config.guild_name}: {e}")
async def send_monday_video():
"""Legacy function - now sends to all servers"""
@@ -61,7 +64,7 @@ def load_last_bedtime_targets():
with open(BEDTIME_TRACKING_FILE, "r") as f:
return json.load(f)
except Exception as e:
print(f"⚠️ Failed to load bedtime tracking file: {e}")
logger.error(f"Failed to load bedtime tracking file: {e}")
return {}
_last_bedtime_targets = load_last_bedtime_targets()
@@ -71,13 +74,13 @@ def save_last_bedtime_targets(data):
with open(BEDTIME_TRACKING_FILE, "w") as f:
json.dump(data, f)
except Exception as e:
print(f"⚠️ Failed to save bedtime tracking file: {e}")
logger.error(f"Failed to save bedtime tracking file: {e}")
async def send_bedtime_reminder_for_server(guild_id: int, client=None):
"""Send bedtime reminder for a specific server"""
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
# Use provided client or fall back to globals.client
@@ -85,7 +88,7 @@ async def send_bedtime_reminder_for_server(guild_id: int, client=None):
client = globals.client
if client is None:
print(f"⚠️ No Discord client available for bedtime reminder in server {guild_id}")
logger.error(f"No Discord client available for bedtime reminder in server {guild_id}")
return
# No need to switch model - llama-swap handles this automatically
@@ -94,7 +97,7 @@ async def send_bedtime_reminder_for_server(guild_id: int, client=None):
for channel_id in server_config.bedtime_channel_ids:
channel = client.get_channel(channel_id)
if not channel:
print(f"⚠️ Channel ID {channel_id} not found in server {server_config.guild_name}")
logger.warning(f"Channel ID {channel_id} not found in server {server_config.guild_name}")
continue
guild = channel.guild
@@ -112,7 +115,8 @@ async def send_bedtime_reminder_for_server(guild_id: int, client=None):
online_members.append(specific_user)
if not online_members:
print(f"😴 No online members to ping in {guild.name}")
# TODO: Handle this in a different way in the future
logger.debug(f"No online members to ping in {guild.name}")
continue
# Avoid repeating the same person unless they're the only one
@@ -162,9 +166,9 @@ async def send_bedtime_reminder_for_server(guild_id: int, client=None):
try:
await channel.send(f"{chosen_one.mention} {bedtime_message}")
print(f"🌙 Sent bedtime reminder to {chosen_one.display_name} in server {server_config.guild_name}")
logger.info(f"Sent bedtime reminder to {chosen_one.display_name} in server {server_config.guild_name}")
except Exception as e:
print(f"⚠️ Failed to send bedtime reminder in server {server_config.guild_name}: {e}")
logger.error(f"Failed to send bedtime reminder in server {server_config.guild_name}: {e}")
async def send_bedtime_reminder():
"""Legacy function - now sends to all servers"""
@@ -176,7 +180,7 @@ def schedule_random_bedtime():
for guild_id in server_manager.servers:
# Schedule bedtime for each server using the async function
# This will be called from the server manager's event loop
print(f"Scheduling bedtime for server {guild_id}")
logger.info(f"Scheduling bedtime for server {guild_id}")
# Note: This function is now called from the server manager's context
# which properly handles the async operations
@@ -188,8 +192,8 @@ async def send_bedtime_now():
async def run_daily_dm_analysis():
"""Run daily DM interaction analysis - reports one user per day"""
if dm_analyzer is None:
print("⚠️ DM Analyzer not initialized, skipping daily analysis")
logger.warning("DM Analyzer not initialized, skipping daily analysis")
return
print("📊 Running daily DM interaction analysis...")
logger.info("Running daily DM interaction analysis...")
await dm_analyzer.run_daily_analysis()

View File

@@ -1,4 +1,7 @@
from utils.llm import query_llama
from utils.logger import get_logger
logger = get_logger('sentiment')
async def analyze_sentiment(messages: list) -> tuple[str, float]:
"""
@@ -40,5 +43,5 @@ Response:"""
return summary, score
except Exception as e:
print(f"Error in sentiment analysis: {e}")
logger.error(f"Error in sentiment analysis: {e}")
return "Error analyzing sentiment", 0.5

View File

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

View File

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