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)