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):
|
||||
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:
|
||||
async with message.channel.typing():
|
||||
# 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
|
||||
|
||||
# Initial trigger settings
|
||||
INTERJECTION_COOLDOWN_HARD = 180 # 3 minutes hard block
|
||||
INTERJECTION_COOLDOWN_SOFT = 900 # 15 minutes for full recovery
|
||||
INTERJECTION_COOLDOWN_HARD = 180 # 3 minutes hard block PER CHANNEL
|
||||
INTERJECTION_COOLDOWN_SOFT = 900 # 15 minutes for full recovery PER CHANNEL
|
||||
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)
|
||||
# ============================================================================
|
||||
@@ -60,6 +65,8 @@ class InterjectionScorer:
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._cooldowns = {} # Per-channel cooldown timestamps
|
||||
cls._instance._streaks = {} # Per-channel near-miss streaks
|
||||
return cls._instance
|
||||
|
||||
@property
|
||||
@@ -94,8 +101,9 @@ class InterjectionScorer:
|
||||
if not self._passes_basic_filter(message):
|
||||
return False, "basic_filter_failed", 0.0
|
||||
|
||||
# Check cooldown
|
||||
cooldown_mult = self._check_cooldown()
|
||||
# Check per-channel cooldown
|
||||
channel_id = message.channel.id
|
||||
cooldown_mult = self._check_cooldown(channel_id)
|
||||
if cooldown_mult == 0.0:
|
||||
return False, "cooldown_active", 0.0
|
||||
|
||||
@@ -146,10 +154,17 @@ class InterjectionScorer:
|
||||
# Apply cooldown multiplier
|
||||
score *= cooldown_mult
|
||||
|
||||
# Check conversation streak (near-misses that build toward a trigger)
|
||||
streak_triggered = self._check_streak(channel_id, score)
|
||||
|
||||
# Decision
|
||||
should_interject = score >= INTERJECTION_THRESHOLD
|
||||
should_interject = score >= INTERJECTION_THRESHOLD or streak_triggered
|
||||
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:
|
||||
logger.info(f"{opposite_persona.upper()} WILL INTERJECT (score: {score:.2f})")
|
||||
logger.info(f" Reasons: {reason_str}")
|
||||
@@ -198,18 +213,22 @@ class InterjectionScorer:
|
||||
if opposite_persona == "evil":
|
||||
# Things Evil Miku can't resist commenting on
|
||||
TRIGGER_TOPICS = {
|
||||
"optimism": ["happiness", "joy", "love", "kindness", "hope", "dreams", "wonderful", "amazing"],
|
||||
"morality": ["good", "should", "must", "right thing", "deserve", "fair", "justice"],
|
||||
"weakness": ["scared", "nervous", "worried", "unsure", "help me", "don't know"],
|
||||
"innocence": ["innocent", "pure", "sweet", "cute", "wholesome", "precious"],
|
||||
"optimism": ["happiness", "joy", "love", "kindness", "hope", "dreams", "wonderful", "amazing", "blessed", "grateful"],
|
||||
"morality": ["good", "should", "must", "right thing", "deserve", "fair", "justice", "the right", "better person"],
|
||||
"weakness": ["scared", "nervous", "worried", "unsure", "help me", "don't know", "confused", "lost", "lonely", "alone"],
|
||||
"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:
|
||||
# Things Miku can't ignore
|
||||
TRIGGER_TOPICS = {
|
||||
"negativity": ["hate", "terrible", "awful", "worst", "horrible", "disgusting", "pathetic"],
|
||||
"cruelty": ["deserve pain", "suffer", "worthless", "stupid", "idiot", "fool"],
|
||||
"hopelessness": ["no point", "meaningless", "nobody cares", "why bother", "give up"],
|
||||
"evil_gloating": ["foolish", "naive", "weak", "inferior", "pathetic"],
|
||||
"negativity": ["hate", "terrible", "awful", "worst", "horrible", "disgusting", "pathetic", "ugly", "boring", "annoying"],
|
||||
"cruelty": ["deserve pain", "suffer", "worthless", "stupid", "idiot", "fool", "moron", "loser", "nobody"],
|
||||
"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", "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
|
||||
@@ -217,7 +236,7 @@ class InterjectionScorer:
|
||||
matches = sum(1 for keyword in keywords if keyword in content_lower)
|
||||
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:
|
||||
"""Check emotional intensity using sentiment analysis"""
|
||||
@@ -300,13 +319,11 @@ class InterjectionScorer:
|
||||
|
||||
return min(score, 1.0)
|
||||
|
||||
def _check_cooldown(self) -> float:
|
||||
"""Check cooldown and return multiplier (0.0 = blocked, 1.0 = full)"""
|
||||
if not hasattr(globals, 'LAST_PERSONA_DIALOGUE_TIME'):
|
||||
globals.LAST_PERSONA_DIALOGUE_TIME = 0
|
||||
|
||||
def _check_cooldown(self, channel_id: int) -> float:
|
||||
"""Check per-channel cooldown and return multiplier (0.0 = blocked, 1.0 = full)"""
|
||||
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:
|
||||
return 0.0
|
||||
@@ -314,6 +331,35 @@ class InterjectionScorer:
|
||||
return (time_since_last - INTERJECTION_COOLDOWN_HARD) / (INTERJECTION_COOLDOWN_SOFT - INTERJECTION_COOLDOWN_HARD)
|
||||
else:
|
||||
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,
|
||||
}
|
||||
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}")
|
||||
return state
|
||||
|
||||
@@ -393,8 +441,8 @@ class PersonaDialogue:
|
||||
Returns delta to add to current tension score.
|
||||
"""
|
||||
|
||||
# Sentiment analysis
|
||||
base_delta = 0.0
|
||||
# Natural tension decay — conversations cool off over time
|
||||
base_delta = -0.03
|
||||
|
||||
if self.sentiment_analyzer:
|
||||
try:
|
||||
@@ -405,13 +453,13 @@ class PersonaDialogue:
|
||||
if is_negative:
|
||||
base_delta = sentiment_score * 0.15
|
||||
else:
|
||||
base_delta = -sentiment_score * 0.05
|
||||
base_delta = -sentiment_score * 0.08 # Stronger cooling for positive
|
||||
except Exception as e:
|
||||
logger.error(f"Sentiment analysis error in tension calc: {e}")
|
||||
|
||||
text_lower = response_text.lower()
|
||||
|
||||
# Escalation patterns
|
||||
# Escalation patterns (reduced weight: 0.05 per match)
|
||||
escalation_patterns = {
|
||||
"insult": ["idiot", "stupid", "pathetic", "fool", "naive", "worthless", "disgusting", "moron"],
|
||||
"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"],
|
||||
}
|
||||
|
||||
# De-escalation patterns
|
||||
# De-escalation patterns (increased weight: -0.08 per match)
|
||||
deescalation_patterns = {
|
||||
"concession": ["you're right", "fair point", "i suppose", "maybe you have", "good point"],
|
||||
"softening": ["i understand", "let's calm", "didn't mean", "sorry", "apologize"],
|
||||
"deflection": ["anyway", "moving on", "whatever you say", "agree to disagree", "let's just"],
|
||||
"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", "maybe we should"],
|
||||
}
|
||||
|
||||
# Check escalation
|
||||
for category, patterns in escalation_patterns.items():
|
||||
matches = sum(1 for p in patterns if p in text_lower)
|
||||
if matches > 0:
|
||||
base_delta += matches * 0.08
|
||||
base_delta += matches * 0.05 # Reduced from 0.08
|
||||
|
||||
# Check de-escalation
|
||||
for category, patterns in deescalation_patterns.items():
|
||||
matches = sum(1 for p in patterns if p in text_lower)
|
||||
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('!')
|
||||
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:
|
||||
base_delta *= 1.3
|
||||
base_delta *= 1.2 # Reduced from 1.3
|
||||
|
||||
# Momentum factor
|
||||
# Momentum factor (reduced)
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user