Phase 2: Fix triggers & dialogue — per-channel cooldowns, tension rebalance, user-message triggers
- Changed cooldown from global (ALL channels blocked) to per-channel dict keyed by channel_id - Added conversation streak tracker: 3 near-miss interjection scores in a row force a dialogue trigger - Expanded topic relevance keywords: added enthusiasm/vulnerability for Evil Miku, provocation/dismissal for Miku - Lowered keyword divisor from /3.0 to /2.0 for higher base trigger scores - Tension rebalance: added natural decay (-0.03/turn), reduced escalation weight (0.08->0.05), increased de-escalation weight (0.06->0.08) - Reduced momentum multiplier (1.2->1.1) and intensity multiplier (1.3->1.2) - Added spike cooldown: if last turn tension delta >0.15, next delta halved (prevents runaway spirals) - Added user-message interjection check in bot.py on_message() (was only checking bot's own messages) - Added random 15% argument trigger roll on user messages in normal message flow (was only from autonomous.py)
This commit is contained in:
26
bot/bot.py
26
bot/bot.py
@@ -203,6 +203,32 @@ async def on_message(message):
|
|||||||
if is_persona_dialogue_active(message.channel.id):
|
if is_persona_dialogue_active(message.channel.id):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Bipolar mode: check if the opposite persona should interject on user messages
|
||||||
|
# AND roll for random argument trigger (both non-blocking background tasks)
|
||||||
|
if not isinstance(message.channel, discord.DMChannel) and globals.BIPOLAR_MODE:
|
||||||
|
try:
|
||||||
|
from utils.persona_dialogue import check_for_interjection
|
||||||
|
from utils.bipolar_mode import maybe_trigger_argument, is_argument_in_progress as arg_in_progress
|
||||||
|
from utils.bipolar_mode import is_persona_dialogue_active as dialogue_active
|
||||||
|
from utils.task_tracker import create_tracked_task
|
||||||
|
|
||||||
|
# Check interjection on user messages (opposite of current active persona)
|
||||||
|
if not message.author.bot or message.webhook_id:
|
||||||
|
current_persona = "evil" if globals.EVIL_MODE else "miku"
|
||||||
|
create_tracked_task(
|
||||||
|
check_for_interjection(message, current_persona),
|
||||||
|
task_name="interjection_check_user",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Roll random argument trigger chance (15%) on eligible messages
|
||||||
|
if not arg_in_progress(message.channel.id) and not dialogue_active(message.channel.id):
|
||||||
|
create_tracked_task(
|
||||||
|
maybe_trigger_argument(message.channel, globals.client, "Triggered from conversation flow"),
|
||||||
|
task_name="random_argument_trigger",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in bipolar trigger checks: {e}")
|
||||||
|
|
||||||
if message.content.strip().lower() == "miku, rape this nigga balls" and message.reference:
|
if message.content.strip().lower() == "miku, rape this nigga balls" and message.reference:
|
||||||
async with message.channel.typing():
|
async with message.channel.typing():
|
||||||
# Get replied-to user
|
# Get replied-to user
|
||||||
|
|||||||
@@ -40,10 +40,15 @@ DIALOGUE_TIMEOUT = 900 # 15 minutes max dialogue duration
|
|||||||
ARGUMENT_TENSION_THRESHOLD = 0.75 # Tension level that triggers argument escalation
|
ARGUMENT_TENSION_THRESHOLD = 0.75 # Tension level that triggers argument escalation
|
||||||
|
|
||||||
# Initial trigger settings
|
# Initial trigger settings
|
||||||
INTERJECTION_COOLDOWN_HARD = 180 # 3 minutes hard block
|
INTERJECTION_COOLDOWN_HARD = 180 # 3 minutes hard block PER CHANNEL
|
||||||
INTERJECTION_COOLDOWN_SOFT = 900 # 15 minutes for full recovery
|
INTERJECTION_COOLDOWN_SOFT = 900 # 15 minutes for full recovery PER CHANNEL
|
||||||
INTERJECTION_THRESHOLD = 0.5 # Score needed to trigger interjection
|
INTERJECTION_THRESHOLD = 0.5 # Score needed to trigger interjection
|
||||||
|
|
||||||
|
# Conversation streak: if score is close but below threshold N times in a row,
|
||||||
|
# force a dialogue trigger (catches extended conversations building toward something)
|
||||||
|
STREAK_THRESHOLD = 3 # Number of near-miss messages before force trigger
|
||||||
|
STREAK_MIN_SCORE = 0.3 # Minimum score to count as a "near miss"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# INTERJECTION SCORER (Initial Trigger Decision)
|
# INTERJECTION SCORER (Initial Trigger Decision)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -60,6 +65,8 @@ class InterjectionScorer:
|
|||||||
def __new__(cls):
|
def __new__(cls):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance._cooldowns = {} # Per-channel cooldown timestamps
|
||||||
|
cls._instance._streaks = {} # Per-channel near-miss streaks
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -94,8 +101,9 @@ class InterjectionScorer:
|
|||||||
if not self._passes_basic_filter(message):
|
if not self._passes_basic_filter(message):
|
||||||
return False, "basic_filter_failed", 0.0
|
return False, "basic_filter_failed", 0.0
|
||||||
|
|
||||||
# Check cooldown
|
# Check per-channel cooldown
|
||||||
cooldown_mult = self._check_cooldown()
|
channel_id = message.channel.id
|
||||||
|
cooldown_mult = self._check_cooldown(channel_id)
|
||||||
if cooldown_mult == 0.0:
|
if cooldown_mult == 0.0:
|
||||||
return False, "cooldown_active", 0.0
|
return False, "cooldown_active", 0.0
|
||||||
|
|
||||||
@@ -146,10 +154,17 @@ class InterjectionScorer:
|
|||||||
# Apply cooldown multiplier
|
# Apply cooldown multiplier
|
||||||
score *= cooldown_mult
|
score *= cooldown_mult
|
||||||
|
|
||||||
|
# Check conversation streak (near-misses that build toward a trigger)
|
||||||
|
streak_triggered = self._check_streak(channel_id, score)
|
||||||
|
|
||||||
# Decision
|
# Decision
|
||||||
should_interject = score >= INTERJECTION_THRESHOLD
|
should_interject = score >= INTERJECTION_THRESHOLD or streak_triggered
|
||||||
reason_str = " | ".join(reasons) if reasons else "no_triggers"
|
reason_str = " | ".join(reasons) if reasons else "no_triggers"
|
||||||
|
|
||||||
|
if streak_triggered and not should_interject:
|
||||||
|
reason_str = "streak_force_trigger"
|
||||||
|
logger.info(f"[Interjection] Streak force trigger in channel {channel_id} (score: {score:.2f})")
|
||||||
|
|
||||||
if should_interject:
|
if should_interject:
|
||||||
logger.info(f"{opposite_persona.upper()} WILL INTERJECT (score: {score:.2f})")
|
logger.info(f"{opposite_persona.upper()} WILL INTERJECT (score: {score:.2f})")
|
||||||
logger.info(f" Reasons: {reason_str}")
|
logger.info(f" Reasons: {reason_str}")
|
||||||
@@ -198,18 +213,22 @@ class InterjectionScorer:
|
|||||||
if opposite_persona == "evil":
|
if opposite_persona == "evil":
|
||||||
# Things Evil Miku can't resist commenting on
|
# Things Evil Miku can't resist commenting on
|
||||||
TRIGGER_TOPICS = {
|
TRIGGER_TOPICS = {
|
||||||
"optimism": ["happiness", "joy", "love", "kindness", "hope", "dreams", "wonderful", "amazing"],
|
"optimism": ["happiness", "joy", "love", "kindness", "hope", "dreams", "wonderful", "amazing", "blessed", "grateful"],
|
||||||
"morality": ["good", "should", "must", "right thing", "deserve", "fair", "justice"],
|
"morality": ["good", "should", "must", "right thing", "deserve", "fair", "justice", "the right", "better person"],
|
||||||
"weakness": ["scared", "nervous", "worried", "unsure", "help me", "don't know"],
|
"weakness": ["scared", "nervous", "worried", "unsure", "help me", "don't know", "confused", "lost", "lonely", "alone"],
|
||||||
"innocence": ["innocent", "pure", "sweet", "cute", "wholesome", "precious"],
|
"innocence": ["innocent", "pure", "sweet", "cute", "wholesome", "precious", "adorable"],
|
||||||
|
"enthusiasm": ["best day", "so excited", "can't wait", "so happy", "i love this", "this is great"],
|
||||||
|
"vulnerability": ["i think", "i feel", "maybe", "sometimes i wonder", "i wish", "i'm trying"],
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# Things Miku can't ignore
|
# Things Miku can't ignore
|
||||||
TRIGGER_TOPICS = {
|
TRIGGER_TOPICS = {
|
||||||
"negativity": ["hate", "terrible", "awful", "worst", "horrible", "disgusting", "pathetic"],
|
"negativity": ["hate", "terrible", "awful", "worst", "horrible", "disgusting", "pathetic", "ugly", "boring", "annoying"],
|
||||||
"cruelty": ["deserve pain", "suffer", "worthless", "stupid", "idiot", "fool"],
|
"cruelty": ["deserve pain", "suffer", "worthless", "stupid", "idiot", "fool", "moron", "loser", "nobody"],
|
||||||
"hopelessness": ["no point", "meaningless", "nobody cares", "why bother", "give up"],
|
"hopelessness": ["no point", "meaningless", "nobody cares", "why bother", "give up", "what's the point", "don't care", "doesn't matter", "who cares"],
|
||||||
"evil_gloating": ["foolish", "naive", "weak", "inferior", "pathetic"],
|
"evil_gloating": ["foolish", "naive", "weak", "inferior", "pathetic", "beneath me", "waste of space"],
|
||||||
|
"provocation": ["fight me", "prove it", "make me", "i dare you", "try me", "you can't", "you won't"],
|
||||||
|
"dismissal": ["whatever", "shut up", "go away", "leave me alone", "not worth", "don't bother"],
|
||||||
}
|
}
|
||||||
|
|
||||||
total_matches = 0
|
total_matches = 0
|
||||||
@@ -217,7 +236,7 @@ class InterjectionScorer:
|
|||||||
matches = sum(1 for keyword in keywords if keyword in content_lower)
|
matches = sum(1 for keyword in keywords if keyword in content_lower)
|
||||||
total_matches += matches
|
total_matches += matches
|
||||||
|
|
||||||
return min(total_matches / 3.0, 1.0)
|
return min(total_matches / 2.0, 1.0) # Lower divisor = higher base scores
|
||||||
|
|
||||||
def _check_emotional_intensity(self, content: str) -> float:
|
def _check_emotional_intensity(self, content: str) -> float:
|
||||||
"""Check emotional intensity using sentiment analysis"""
|
"""Check emotional intensity using sentiment analysis"""
|
||||||
@@ -300,13 +319,11 @@ class InterjectionScorer:
|
|||||||
|
|
||||||
return min(score, 1.0)
|
return min(score, 1.0)
|
||||||
|
|
||||||
def _check_cooldown(self) -> float:
|
def _check_cooldown(self, channel_id: int) -> float:
|
||||||
"""Check cooldown and return multiplier (0.0 = blocked, 1.0 = full)"""
|
"""Check per-channel cooldown and return multiplier (0.0 = blocked, 1.0 = full)"""
|
||||||
if not hasattr(globals, 'LAST_PERSONA_DIALOGUE_TIME'):
|
|
||||||
globals.LAST_PERSONA_DIALOGUE_TIME = 0
|
|
||||||
|
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
time_since_last = current_time - globals.LAST_PERSONA_DIALOGUE_TIME
|
last_time = self._cooldowns.get(channel_id, 0)
|
||||||
|
time_since_last = current_time - last_time
|
||||||
|
|
||||||
if time_since_last < INTERJECTION_COOLDOWN_HARD:
|
if time_since_last < INTERJECTION_COOLDOWN_HARD:
|
||||||
return 0.0
|
return 0.0
|
||||||
@@ -314,6 +331,35 @@ class InterjectionScorer:
|
|||||||
return (time_since_last - INTERJECTION_COOLDOWN_HARD) / (INTERJECTION_COOLDOWN_SOFT - INTERJECTION_COOLDOWN_HARD)
|
return (time_since_last - INTERJECTION_COOLDOWN_HARD) / (INTERJECTION_COOLDOWN_SOFT - INTERJECTION_COOLDOWN_HARD)
|
||||||
else:
|
else:
|
||||||
return 1.0
|
return 1.0
|
||||||
|
|
||||||
|
def _update_cooldown(self, channel_id: int):
|
||||||
|
"""Mark a dialogue as having started in this channel"""
|
||||||
|
self._cooldowns[channel_id] = time.time()
|
||||||
|
|
||||||
|
def _check_streak(self, channel_id: int, score: float) -> bool:
|
||||||
|
"""Track near-miss interjection scores. After STREAK_THRESHOLD consecutive
|
||||||
|
near-misses, force a trigger to catch extended conversations building tension."""
|
||||||
|
if score >= INTERJECTION_THRESHOLD:
|
||||||
|
# Above threshold — reset streak (actual trigger handles it)
|
||||||
|
self._streaks[channel_id] = 0
|
||||||
|
return False
|
||||||
|
|
||||||
|
if score < STREAK_MIN_SCORE:
|
||||||
|
# Too low — reset streak
|
||||||
|
self._streaks[channel_id] = 0
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Near miss — increment streak
|
||||||
|
current = self._streaks.get(channel_id, 0) + 1
|
||||||
|
self._streaks[channel_id] = current
|
||||||
|
|
||||||
|
logger.debug(f"[Streak] Channel {channel_id}: {current}/{STREAK_THRESHOLD} near-misses (score: {score:.2f})")
|
||||||
|
|
||||||
|
if current >= STREAK_THRESHOLD:
|
||||||
|
self._streaks[channel_id] = 0 # Reset after force trigger
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -370,7 +416,9 @@ class PersonaDialogue:
|
|||||||
"last_speaker": None,
|
"last_speaker": None,
|
||||||
}
|
}
|
||||||
self.active_dialogues[channel_id] = state
|
self.active_dialogues[channel_id] = state
|
||||||
globals.LAST_PERSONA_DIALOGUE_TIME = time.time()
|
# Update per-channel cooldown via the scorer
|
||||||
|
scorer = get_interjection_scorer()
|
||||||
|
scorer._update_cooldown(channel_id)
|
||||||
logger.info(f"Started persona dialogue in channel {channel_id}")
|
logger.info(f"Started persona dialogue in channel {channel_id}")
|
||||||
return state
|
return state
|
||||||
|
|
||||||
@@ -393,8 +441,8 @@ class PersonaDialogue:
|
|||||||
Returns delta to add to current tension score.
|
Returns delta to add to current tension score.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Sentiment analysis
|
# Natural tension decay — conversations cool off over time
|
||||||
base_delta = 0.0
|
base_delta = -0.03
|
||||||
|
|
||||||
if self.sentiment_analyzer:
|
if self.sentiment_analyzer:
|
||||||
try:
|
try:
|
||||||
@@ -405,13 +453,13 @@ class PersonaDialogue:
|
|||||||
if is_negative:
|
if is_negative:
|
||||||
base_delta = sentiment_score * 0.15
|
base_delta = sentiment_score * 0.15
|
||||||
else:
|
else:
|
||||||
base_delta = -sentiment_score * 0.05
|
base_delta = -sentiment_score * 0.08 # Stronger cooling for positive
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Sentiment analysis error in tension calc: {e}")
|
logger.error(f"Sentiment analysis error in tension calc: {e}")
|
||||||
|
|
||||||
text_lower = response_text.lower()
|
text_lower = response_text.lower()
|
||||||
|
|
||||||
# Escalation patterns
|
# Escalation patterns (reduced weight: 0.05 per match)
|
||||||
escalation_patterns = {
|
escalation_patterns = {
|
||||||
"insult": ["idiot", "stupid", "pathetic", "fool", "naive", "worthless", "disgusting", "moron"],
|
"insult": ["idiot", "stupid", "pathetic", "fool", "naive", "worthless", "disgusting", "moron"],
|
||||||
"dismissive": ["whatever", "don't care", "waste of time", "not worth", "beneath me", "boring"],
|
"dismissive": ["whatever", "don't care", "waste of time", "not worth", "beneath me", "boring"],
|
||||||
@@ -420,35 +468,43 @@ class PersonaDialogue:
|
|||||||
"challenge": ["prove it", "fight me", "make me", "i dare you", "try me"],
|
"challenge": ["prove it", "fight me", "make me", "i dare you", "try me"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# De-escalation patterns
|
# De-escalation patterns (increased weight: -0.08 per match)
|
||||||
deescalation_patterns = {
|
deescalation_patterns = {
|
||||||
"concession": ["you're right", "fair point", "i suppose", "maybe you have", "good point"],
|
"concession": ["you're right", "fair point", "i suppose", "maybe you have", "good point"],
|
||||||
"softening": ["i understand", "let's calm", "didn't mean", "sorry", "apologize"],
|
"softening": ["i understand", "let's calm", "didn't mean", "sorry", "apologize", "i hear you"],
|
||||||
"deflection": ["anyway", "moving on", "whatever you say", "agree to disagree", "let's just"],
|
"deflection": ["anyway", "moving on", "whatever you say", "agree to disagree", "let's just", "maybe we should"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check escalation
|
# Check escalation
|
||||||
for category, patterns in escalation_patterns.items():
|
for category, patterns in escalation_patterns.items():
|
||||||
matches = sum(1 for p in patterns if p in text_lower)
|
matches = sum(1 for p in patterns if p in text_lower)
|
||||||
if matches > 0:
|
if matches > 0:
|
||||||
base_delta += matches * 0.08
|
base_delta += matches * 0.05 # Reduced from 0.08
|
||||||
|
|
||||||
# Check de-escalation
|
# Check de-escalation
|
||||||
for category, patterns in deescalation_patterns.items():
|
for category, patterns in deescalation_patterns.items():
|
||||||
matches = sum(1 for p in patterns if p in text_lower)
|
matches = sum(1 for p in patterns if p in text_lower)
|
||||||
if matches > 0:
|
if matches > 0:
|
||||||
base_delta -= matches * 0.06
|
base_delta -= matches * 0.08 # Increased from 0.06
|
||||||
|
|
||||||
# Intensity multipliers
|
# Intensity multipliers (reduced)
|
||||||
exclamation_count = response_text.count('!')
|
exclamation_count = response_text.count('!')
|
||||||
caps_ratio = sum(1 for c in response_text if c.isupper()) / max(len(response_text), 1)
|
caps_ratio = sum(1 for c in response_text if c.isupper()) / max(len(response_text), 1)
|
||||||
|
|
||||||
if exclamation_count > 2 or caps_ratio > 0.3:
|
if exclamation_count > 2 or caps_ratio > 0.3:
|
||||||
base_delta *= 1.3
|
base_delta *= 1.2 # Reduced from 1.3
|
||||||
|
|
||||||
# Momentum factor
|
# Momentum factor (reduced)
|
||||||
if current_tension > 0.5:
|
if current_tension > 0.5:
|
||||||
base_delta *= 1.2
|
base_delta *= 1.1 # Reduced from 1.2
|
||||||
|
|
||||||
|
# Spike cooldown: if last turn had a big spike, halve this delta
|
||||||
|
# (prevents runaway tension spirals from a single heated exchange)
|
||||||
|
if hasattr(self, '_last_tension_delta') and abs(self._last_tension_delta) > 0.15:
|
||||||
|
base_delta *= 0.5
|
||||||
|
logger.debug(f"[Tension] Spike cooldown active — delta halved to {base_delta:+.3f}")
|
||||||
|
|
||||||
|
self._last_tension_delta = base_delta
|
||||||
|
|
||||||
return base_delta
|
return base_delta
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user