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