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:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user