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

@@ -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)