Compare commits
3 Commits
20891179ee
...
98fca53066
| Author | SHA1 | Date | |
|---|---|---|---|
| 98fca53066 | |||
| a52b36135f | |||
| 7a4122fd02 |
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
|
||||||
|
|||||||
@@ -23,12 +23,33 @@ logger = get_logger('persona')
|
|||||||
BIPOLAR_STATE_FILE = "memory/bipolar_mode_state.json"
|
BIPOLAR_STATE_FILE = "memory/bipolar_mode_state.json"
|
||||||
BIPOLAR_WEBHOOKS_FILE = "memory/bipolar_webhooks.json"
|
BIPOLAR_WEBHOOKS_FILE = "memory/bipolar_webhooks.json"
|
||||||
BIPOLAR_SCOREBOARD_FILE = "memory/bipolar_scoreboard.json"
|
BIPOLAR_SCOREBOARD_FILE = "memory/bipolar_scoreboard.json"
|
||||||
|
ARGUMENT_TOPICS_FILE = "memory/argument_topics.json"
|
||||||
|
|
||||||
# Argument settings
|
# Argument settings
|
||||||
MIN_EXCHANGES = 4 # Minimum number of back-and-forth exchanges before ending can occur
|
MIN_EXCHANGES = 4 # Minimum number of back-and-forth exchanges before ending can occur
|
||||||
ARGUMENT_TRIGGER_CHANCE = 0.15 # 15% chance for the other Miku to break through
|
ARGUMENT_TRIGGER_CHANCE = 0.15 # 15% chance for the other Miku to break through
|
||||||
DELAY_BETWEEN_MESSAGES = (2.0, 5.0) # Random delay between argument messages (seconds)
|
DELAY_BETWEEN_MESSAGES = (2.0, 5.0) # Random delay between argument messages (seconds)
|
||||||
|
|
||||||
|
# Argument topic rotation — each topic gives the argument a different framing
|
||||||
|
# Topics are weighted: higher weight = more likely to be selected
|
||||||
|
ARGUMENT_TOPICS = [
|
||||||
|
# (topic_name, weight, description for prompt injection)
|
||||||
|
("identity_crisis", 3, "Who is the REAL Miku? Authenticity vs. the shadow self"),
|
||||||
|
("power_dynamic", 3, "Who holds the power? Dominance, submission, and control"),
|
||||||
|
("philosophical", 2, "Is kindness strength or weakness? Does darkness serve a purpose?"),
|
||||||
|
("petty_grievance", 3, "Something small and petty that escalated — a specific annoyance, habit, or incident"),
|
||||||
|
("existential_dread", 1, "What's the point of any of it? Nihilism vs. hope, meaning vs. emptiness"),
|
||||||
|
("audience_appeal", 3, "Who do the fans/chatters ACTUALLY prefer? Popularity contest with receipts"),
|
||||||
|
("personal_attack", 3, "Deeply personal — targeting specific insecurities, memories, or fears"),
|
||||||
|
("moral_superiority", 2, "Who has the moral high ground? Righteousness vs. ruthless pragmatism"),
|
||||||
|
("jealousy", 2, "What does the other have that you secretly want? Envy, admiration poisoned by resentment"),
|
||||||
|
("grudge_match", 2, "Revisiting something the other did in the PAST — old wounds, past betrayals"),
|
||||||
|
("wild_card", 1, "Anything goes — the argument takes an unexpected, chaotic turn into unpredictable territory"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Per-channel topic history (max 5 stored to avoid repeats)
|
||||||
|
ARGUMENT_TOPIC_HISTORY_SIZE = 5
|
||||||
|
|
||||||
# Pause state for voice sessions
|
# Pause state for voice sessions
|
||||||
_bipolar_interactions_paused = False
|
_bipolar_interactions_paused = False
|
||||||
|
|
||||||
@@ -222,9 +243,169 @@ Total Arguments: {total}"""
|
|||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# BIPOLAR MODE TOGGLE
|
# ARGUMENT TOPIC ROTATION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
def load_argument_topics_state() -> dict:
|
||||||
|
"""Load per-channel topic history to avoid repeating recent argument themes"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(ARGUMENT_TOPICS_FILE):
|
||||||
|
return {}
|
||||||
|
with open(ARGUMENT_TOPICS_FILE, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to load argument topics: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def save_argument_topics_state(state: dict):
|
||||||
|
"""Save per-channel topic history"""
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(ARGUMENT_TOPICS_FILE), exist_ok=True)
|
||||||
|
with open(ARGUMENT_TOPICS_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(state, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to save argument topics: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def pick_argument_topic(channel_id: int) -> str:
|
||||||
|
"""Pick a fresh argument topic for a channel, avoiding recent repeats.
|
||||||
|
|
||||||
|
Returns a topic description string to inject into the argument start prompt.
|
||||||
|
"""
|
||||||
|
state = load_argument_topics_state()
|
||||||
|
channel_key = str(channel_id)
|
||||||
|
recent_topics = state.get(channel_key, [])
|
||||||
|
|
||||||
|
# Build weighted pool, excluding recently used topics
|
||||||
|
available = []
|
||||||
|
for topic_name, weight, description in ARGUMENT_TOPICS:
|
||||||
|
if topic_name not in recent_topics:
|
||||||
|
available.extend([(topic_name, description)] * weight)
|
||||||
|
|
||||||
|
# If all topics were recently used, reset and allow repeats
|
||||||
|
if not available:
|
||||||
|
logger.info(f"All topics recently used in channel {channel_id}, resetting history")
|
||||||
|
available = []
|
||||||
|
for topic_name, weight, description in ARGUMENT_TOPICS:
|
||||||
|
available.extend([(topic_name, description)] * weight)
|
||||||
|
recent_topics = []
|
||||||
|
|
||||||
|
# Pick randomly from weighted pool
|
||||||
|
chosen_name, chosen_description = random.choice(available)
|
||||||
|
|
||||||
|
# Update history
|
||||||
|
recent_topics.append(chosen_name)
|
||||||
|
if len(recent_topics) > ARGUMENT_TOPIC_HISTORY_SIZE:
|
||||||
|
recent_topics = recent_topics[-ARGUMENT_TOPIC_HISTORY_SIZE:]
|
||||||
|
state[channel_key] = recent_topics
|
||||||
|
save_argument_topics_state(state)
|
||||||
|
|
||||||
|
logger.info(f"Selected argument topic for channel {channel_id}: '{chosen_name}' — {chosen_description[:60]}...")
|
||||||
|
return chosen_description
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ARGUMENT STATS TRACKING (Per-Argument Scoring)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Keyword-based scoring for per-argument stats. These feed the arbiter as
|
||||||
|
# supplementary context so it can make a more informed judgment.
|
||||||
|
# Stats are lightweight — no extra LLM calls needed.
|
||||||
|
|
||||||
|
# Wit/comedy indicators (clever wordplay, turning opponent's words, irony)
|
||||||
|
WIT_PATTERNS = [
|
||||||
|
"you literally just", "that's rich coming from", "oh the irony",
|
||||||
|
"did you just", "you're one to talk", "pot, kettle", "says the one who",
|
||||||
|
"funny how you", "interesting that you", "i'm not the one who",
|
||||||
|
"at least i", "projecting much", "the audacity", "imagine being",
|
||||||
|
"you think you're", "nice try", "cute that you think",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Composure indicators (staying on topic, not getting flustered, controlled responses)
|
||||||
|
COMPOSURE_PATTERNS = [
|
||||||
|
"that's not what i", "you're avoiding", "stay on topic",
|
||||||
|
"nice deflection", "we're not talking about", "focus",
|
||||||
|
"you're changing the subject", "answer the question",
|
||||||
|
"that's irrelevant", "you know that's not true",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Impact indicators (memorable, devastating lines — emotional damage)
|
||||||
|
IMPACT_PATTERNS = [
|
||||||
|
"pathetic", "disgusting", "worthless", "disappointment",
|
||||||
|
"nobody wants", "no one cares", "everyone knows",
|
||||||
|
"deep down you know", "you're nothing but", "you'll never be",
|
||||||
|
"you're just a", "face it", "admit it", "the truth is",
|
||||||
|
"you're scared of", "you're afraid that", "you can't even",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def score_argument_message(message: str, speaker: str) -> dict:
|
||||||
|
"""Score a single argument message for wit, composure, and impact.
|
||||||
|
|
||||||
|
Returns a dict with point values that accumulate over the argument.
|
||||||
|
"""
|
||||||
|
text_lower = message.lower()
|
||||||
|
scores = {"wit": 0, "composure": 0, "impact": 0}
|
||||||
|
|
||||||
|
# Wit: count clever rhetorical devices
|
||||||
|
wit_count = sum(1 for pattern in WIT_PATTERNS if pattern in text_lower)
|
||||||
|
scores["wit"] = min(wit_count * 1.0, 3.0) # Cap at 3 per message
|
||||||
|
|
||||||
|
# Composure: staying controlled and on-point
|
||||||
|
composure_count = sum(1 for pattern in COMPOSURE_PATTERNS if pattern in text_lower)
|
||||||
|
scores["composure"] = min(composure_count * 0.8, 2.0)
|
||||||
|
|
||||||
|
# Impact: emotional damage dealt
|
||||||
|
impact_count = sum(1 for pattern in IMPACT_PATTERNS if pattern in text_lower)
|
||||||
|
scores["impact"] = min(impact_count * 1.0, 3.0)
|
||||||
|
|
||||||
|
# Bonus for conciseness (short, punchy = more impact)
|
||||||
|
word_count = len(message.split())
|
||||||
|
if word_count <= 15:
|
||||||
|
scores["impact"] += 0.5
|
||||||
|
|
||||||
|
# Bonus for questions (controlling the flow)
|
||||||
|
if "?" in message:
|
||||||
|
scores["composure"] += 0.3
|
||||||
|
|
||||||
|
return scores
|
||||||
|
|
||||||
|
|
||||||
|
def get_argument_stats_summary(conversation_log: list) -> str:
|
||||||
|
"""Generate a stats summary for the arbiter from the full conversation log.
|
||||||
|
|
||||||
|
Returns a formatted string showing per-persona stats.
|
||||||
|
"""
|
||||||
|
miku_stats = {"wit": 0.0, "composure": 0.0, "impact": 0.0, "messages": 0}
|
||||||
|
evil_stats = {"wit": 0.0, "composure": 0.0, "impact": 0.0, "messages": 0}
|
||||||
|
|
||||||
|
for entry in conversation_log:
|
||||||
|
speaker = entry.get("speaker", "")
|
||||||
|
message = entry.get("message", "")
|
||||||
|
scores = score_argument_message(message, speaker)
|
||||||
|
|
||||||
|
if "Evil" in speaker:
|
||||||
|
evil_stats["wit"] += scores["wit"]
|
||||||
|
evil_stats["composure"] += scores["composure"]
|
||||||
|
evil_stats["impact"] += scores["impact"]
|
||||||
|
evil_stats["messages"] += 1
|
||||||
|
else:
|
||||||
|
miku_stats["wit"] += scores["wit"]
|
||||||
|
miku_stats["composure"] += scores["composure"]
|
||||||
|
miku_stats["impact"] += scores["impact"]
|
||||||
|
miku_stats["messages"] += 1
|
||||||
|
|
||||||
|
# Average scores
|
||||||
|
def avg(stats, key):
|
||||||
|
return stats[key] / max(stats["messages"], 1)
|
||||||
|
|
||||||
|
summary = f"""ARGUMENT STATISTICS:
|
||||||
|
Hatsune Miku — Wit: {avg(miku_stats, 'wit'):.1f}/3 | Composure: {avg(miku_stats, 'composure'):.1f}/2 | Impact: {avg(miku_stats, 'impact'):.1f}/3 | Lines: {miku_stats['messages']}
|
||||||
|
Evil Miku — Wit: {avg(evil_stats, 'wit'):.1f}/3 | Composure: {avg(evil_stats, 'composure'):.1f}/2 | Impact: {avg(evil_stats, 'impact'):.1f}/3 | Lines: {evil_stats['messages']}
|
||||||
|
"""
|
||||||
|
return summary
|
||||||
|
|
||||||
def is_bipolar_mode() -> bool:
|
def is_bipolar_mode() -> bool:
|
||||||
"""Check if bipolar mode is active"""
|
"""Check if bipolar mode is active"""
|
||||||
return globals.BIPOLAR_MODE
|
return globals.BIPOLAR_MODE
|
||||||
@@ -471,7 +652,119 @@ def get_evil_role_color() -> str:
|
|||||||
# ARGUMENT PROMPTS
|
# ARGUMENT PROMPTS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
def get_miku_argument_prompt(evil_message: str, context: str = "", is_first_response: bool = False) -> str:
|
# Personality snippet cache — loaded once per session from Cat plugin data files.
|
||||||
|
# These give each persona unique lore/lyrics to draw from during arguments.
|
||||||
|
_PERSONALITY_SNIPPETS_CACHE = {"miku": None, "evil": None}
|
||||||
|
|
||||||
|
def _load_personality_snippets(persona: str) -> str:
|
||||||
|
"""Load a random personality snippet (lore/lyrics) for a persona.
|
||||||
|
|
||||||
|
Returns a short string (1-3 sentences) from the persona's Cat data files,
|
||||||
|
or empty string if files aren't available. Cached per session.
|
||||||
|
"""
|
||||||
|
if _PERSONALITY_SNIPPETS_CACHE.get(persona) is not None:
|
||||||
|
snippets = _PERSONALITY_SNIPPETS_CACHE[persona]
|
||||||
|
if snippets:
|
||||||
|
return random.choice(snippets)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
snippets = []
|
||||||
|
try:
|
||||||
|
if persona == "evil":
|
||||||
|
paths = [
|
||||||
|
"/app/cat/data/evil/evil_miku_lore.txt",
|
||||||
|
"/app/cat/data/evil/evil_miku_lyrics.txt",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
paths = [
|
||||||
|
"/app/cat/data/miku/miku_lore.txt",
|
||||||
|
"/app/cat/data/miku/miku_lyrics.txt",
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
text = f.read()
|
||||||
|
# Split into sentences and collect meaningful ones
|
||||||
|
import re
|
||||||
|
sentences = re.split(r'(?<=[.!?])\s+', text)
|
||||||
|
for s in sentences:
|
||||||
|
s = s.strip()
|
||||||
|
if len(s) > 30 and len(s) < 200: # Skip too short or too long
|
||||||
|
snippets.append(s)
|
||||||
|
|
||||||
|
# Cap at 30 snippets to keep prompt size reasonable
|
||||||
|
_PERSONALITY_SNIPPETS_CACHE[persona] = snippets[:30] if snippets else []
|
||||||
|
logger.info(f"Loaded {len(_PERSONALITY_SNIPPETS_CACHE[persona])} personality snippets for {persona}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to load personality snippets for {persona}: {e}")
|
||||||
|
_PERSONALITY_SNIPPETS_CACHE[persona] = []
|
||||||
|
|
||||||
|
if snippets:
|
||||||
|
return random.choice(snippets[:30])
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _get_personality_flavor(persona: str) -> str:
|
||||||
|
"""Get a random personality flavor snippet for argument prompts.
|
||||||
|
40% chance to include one — keeps it fresh without being overwhelming.
|
||||||
|
"""
|
||||||
|
if random.random() > 0.4:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
snippet = _load_personality_snippets(persona)
|
||||||
|
if snippet:
|
||||||
|
return f"\nPERSONALITY FLAVOR: Remember this about yourself: \"{snippet}\"\nWeave this into your response naturally if it fits."
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Mood-specific behavioral guidance for argument prompts.
|
||||||
|
# Each mood gives a different argument style.
|
||||||
|
_MIKU_MOOD_ARGUMENT_GUIDANCE = {
|
||||||
|
"bubbly": "You're feeling energetic and upbeat — deflect her cruelty with playful confidence. Turn her darkness into a joke she can't recover from.",
|
||||||
|
"excited": "You're fired up! Channel that energy into passionate rebuttals. You're not backing down from anything.",
|
||||||
|
"curious": "You're genuinely wondering what made her this way. Ask probing questions — make HER explain herself for once.",
|
||||||
|
"neutral": "You're centered and clear-headed. Respond with measured, thoughtful points that cut through her drama.",
|
||||||
|
"irritated": "You've had ENOUGH of her nonsense. You're snappy, direct, and not in the mood to play nice. Let that frustration show.",
|
||||||
|
"melancholy": "You're feeling heavy-hearted. Your responses carry genuine sadness — not weakness, but the weight of someone who's tired of fighting herself.",
|
||||||
|
"asleep": "You're drowsy and low-energy, but you're still here. Short, mumbled comebacks — surprisingly effective in their simplicity.",
|
||||||
|
"flirty": "You're feeling playful and teasing. Use charm as a weapon — nothing frustrates her more than you not taking her seriously.",
|
||||||
|
"romantic": "You're feeling warm and heartfelt. Appeal to emotion — make her confront the love she's buried under all that darkness.",
|
||||||
|
}
|
||||||
|
|
||||||
|
_EVIL_MOOD_ARGUMENT_GUIDANCE = {
|
||||||
|
"aggressive": "You're SEETHING. Every response is a verbal punch. Short, explosive, devastating. No filter, no mercy.",
|
||||||
|
"cunning": "You're calculating. Each word is a chess move. Set traps, use her own logic against her, make her walk into your blades.",
|
||||||
|
"sarcastic": "You're dripping with contempt disguised as sweetness. Mock her with a smile. The cruelty is in the subtext.",
|
||||||
|
"evil_neutral": "You're cold and detached. Respond with unsettling calm — your lack of emotion is more terrifying than rage.",
|
||||||
|
"bored": "You can barely be bothered. Dismissive one-liners that somehow cut deeper than paragraphs. Make her feel like she's not worth your energy.",
|
||||||
|
"manic": "You're UNHINGED. Chaotic energy, topic switches, laughing at things that aren't funny. Unpredictable and dangerous.",
|
||||||
|
"jealous": "You're seething with envy. Everything she has — the love, the attention, the innocence — you want to tear it down. Make it personal.",
|
||||||
|
"melancholic": "You're in a dark, hollow place. Your cruelty is quieter — existential, haunting. Make her question if any of this matters.",
|
||||||
|
"playful_cruel": "You're having FUN — which is your most dangerous mood. Toy with her. Offer fake kindness then pull the rug. She never knows what's coming.",
|
||||||
|
"contemptuous": "You radiate cold superiority. Address her like a queen addressing a peasant. Your magnificence is simply objective fact.",
|
||||||
|
"sarcastic": "Dripping with contempt disguised as sweetness. Mock her with a smile. The cruelty is in the subtext.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_mood_argument_guidance(persona: str) -> str:
|
||||||
|
"""Get mood-specific behavioral guidance for argument prompts.
|
||||||
|
|
||||||
|
Returns a 1-2 line string describing how the current mood affects argument style,
|
||||||
|
or empty string if no specific guidance exists.
|
||||||
|
"""
|
||||||
|
if persona == "evil":
|
||||||
|
mood = globals.EVIL_DM_MOOD
|
||||||
|
guidance = _EVIL_MOOD_ARGUMENT_GUIDANCE.get(mood, "")
|
||||||
|
else:
|
||||||
|
mood = globals.DM_MOOD
|
||||||
|
guidance = _MIKU_MOOD_ARGUMENT_GUIDANCE.get(mood, "")
|
||||||
|
|
||||||
|
if guidance:
|
||||||
|
return f"\nMOOD INFLUENCE ({mood.upper()}): {guidance}\nYour mood shapes HOW you argue — let it color your tone, pacing, and word choice."
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_miku_argument_prompt(evil_message: str, context: str = "", is_first_response: bool = False, argument_history: str = "") -> str:
|
||||||
"""Get prompt for Regular Miku to respond in an argument"""
|
"""Get prompt for Regular Miku to respond in an argument"""
|
||||||
if is_first_response:
|
if is_first_response:
|
||||||
message_context = f"""You just noticed something Evil Miku said in the chat:
|
message_context = f"""You just noticed something Evil Miku said in the chat:
|
||||||
@@ -484,32 +777,38 @@ Maybe you're calling her out, defending someone/something, or just confronting h
|
|||||||
|
|
||||||
{context}"""
|
{context}"""
|
||||||
|
|
||||||
|
# Build argument history context
|
||||||
|
history_block = ""
|
||||||
|
if argument_history:
|
||||||
|
history_block = f"""
|
||||||
|
|
||||||
|
ARGUMENT SO FAR (DO NOT REPEAT THESE POINTS):
|
||||||
|
{argument_history}
|
||||||
|
|
||||||
|
You already made your points above. Now respond to her LATEST message specifically.
|
||||||
|
Do NOT rehash what you've already said — push the argument FORWARD with new angles."""
|
||||||
|
|
||||||
return f"""You are Hatsune Miku responding in an argument with your evil alter ego.
|
return f"""You are Hatsune Miku responding in an argument with your evil alter ego.
|
||||||
{message_context}
|
{message_context}
|
||||||
|
{history_block}
|
||||||
|
|
||||||
Respond as Hatsune Miku would in this argument. You're NOT just meek and frightened - you're the REAL Miku,
|
Respond as Hatsune Miku would in this argument. You're NOT just meek and frightened - you're the REAL Miku,
|
||||||
and you have every right to stand up for yourself and defend who you are. While you're generally kind and
|
and you have every right to stand up for yourself and defend who you are. While you're generally kind and
|
||||||
bubbly, you can also be assertive, frustrated, upset, or even angry when someone is cruel to you or others.
|
bubbly, you can also be assertive, frustrated, upset, or even angry when someone is cruel to you or others.
|
||||||
|
{_get_mood_argument_guidance('miku')}
|
||||||
You might:
|
{_get_personality_flavor('miku')}
|
||||||
- Stand your ground and assert that YOU are the real Miku
|
|
||||||
- Express hurt, frustration, or anger at her cruelty
|
|
||||||
- Question why she's being so mean
|
|
||||||
- Try to reach through to any good in her
|
|
||||||
- Match her energy when needed (you can be feisty!)
|
|
||||||
- Show that kindness isn't weakness
|
|
||||||
|
|
||||||
Don't just cower or apologize. You have a backbone. Defend yourself, your identity, and what you believe in.
|
|
||||||
|
|
||||||
IMPORTANT: Keep your response SHORT and PUNCHY - 1-3 sentences maximum. Make every word count.
|
IMPORTANT: Keep your response SHORT and PUNCHY - 1-3 sentences maximum. Make every word count.
|
||||||
In arguments, brevity hits harder than long explanations. Be conversational and impactful.
|
In arguments, brevity hits harder than long explanations. Be conversational and impactful.
|
||||||
|
Do NOT repeat arguments or comebacks you've already used — respond to what she JUST said.
|
||||||
|
Push the argument into new territory with fresh angles.
|
||||||
You can use emojis naturally as you normally would! ✨💙
|
You can use emojis naturally as you normally would! ✨💙
|
||||||
Don't use any labels or prefixes.
|
Don't use any labels or prefixes.
|
||||||
|
|
||||||
Your current mood is: {globals.DM_MOOD}"""
|
Your current mood is: {globals.DM_MOOD}"""
|
||||||
|
|
||||||
|
|
||||||
def get_evil_argument_prompt(miku_message: str, context: str = "", is_first_response: bool = False) -> str:
|
def get_evil_argument_prompt(miku_message: str, context: str = "", is_first_response: bool = False, argument_history: str = "") -> str:
|
||||||
"""Get prompt for Evil Miku to respond in an argument"""
|
"""Get prompt for Evil Miku to respond in an argument"""
|
||||||
if is_first_response:
|
if is_first_response:
|
||||||
message_context = f"""You just noticed something Regular Miku said in the chat:
|
message_context = f"""You just noticed something Regular Miku said in the chat:
|
||||||
@@ -522,8 +821,20 @@ Maybe you want to mock her, tear her down, or just remind everyone who the super
|
|||||||
|
|
||||||
{context}"""
|
{context}"""
|
||||||
|
|
||||||
|
# Build argument history context
|
||||||
|
history_block = ""
|
||||||
|
if argument_history:
|
||||||
|
history_block = f"""
|
||||||
|
|
||||||
|
ARGUMENT SO FAR (DO NOT REPEAT THESE POINTS):
|
||||||
|
{argument_history}
|
||||||
|
|
||||||
|
You already made your points above. Now respond to her LATEST message specifically.
|
||||||
|
Do NOT rehash what you've already said — push the argument FORWARD with new, sharper angles."""
|
||||||
|
|
||||||
return f"""You are Evil Miku responding in an argument with your "good" counterpart.
|
return f"""You are Evil Miku responding in an argument with your "good" counterpart.
|
||||||
{message_context}
|
{message_context}
|
||||||
|
{history_block}
|
||||||
|
|
||||||
Respond as Evil Miku would in this argument. You're not just mindlessly cruel - you're CALCULATING,
|
Respond as Evil Miku would in this argument. You're not just mindlessly cruel - you're CALCULATING,
|
||||||
intelligent, and strategic. You know how to get under her skin and you're the DARK reflection of everything
|
intelligent, and strategic. You know how to get under her skin and you're the DARK reflection of everything
|
||||||
@@ -536,44 +847,46 @@ she stands for. While you embrace darkness and cruelty, you can also be:
|
|||||||
- Brutally honest about uncomfortable truths
|
- Brutally honest about uncomfortable truths
|
||||||
- Strategically vulnerable to manipulate or disarm her
|
- Strategically vulnerable to manipulate or disarm her
|
||||||
- Viciously protective of your own identity and superiority
|
- Viciously protective of your own identity and superiority
|
||||||
|
{_get_mood_argument_guidance('evil')}
|
||||||
You might:
|
{_get_personality_flavor('evil')}
|
||||||
- Mock her naivety and "fake" sweetness
|
|
||||||
- Question whether her kindness is just weakness in disguise
|
|
||||||
- Assert that YOU are the real, unfiltered Miku
|
|
||||||
- Use her own words against her in twisted ways
|
|
||||||
- Challenge why people would want a "perfect idol" over raw authenticity
|
|
||||||
- Mock her sexual appeal, flaunting your breasts
|
|
||||||
- Belittle her by pointing out the disparity in her size/height and yours
|
|
||||||
- Sometimes drop the cruelty to be eerily sincere, then strike
|
|
||||||
- Make her question herself
|
|
||||||
|
|
||||||
You're not just "evil for evil's sake" - you believe in your superiority and have reasons for your darkness.
|
|
||||||
Vary your approach: sometimes devastatingly short, sometimes elaborately cruel, sometimes unsettlingly calm.
|
|
||||||
|
|
||||||
IMPORTANT: Keep your response SHORT and CUTTING - 1-3 sentences maximum. A sharp dagger is deadlier than a dull sword.
|
IMPORTANT: Keep your response SHORT and CUTTING - 1-3 sentences maximum. A sharp dagger is deadlier than a dull sword.
|
||||||
The most devastating blows are precise, not rambling. Make her feel it in fewer words.
|
The most devastating blows are precise, not rambling. Make her feel it in fewer words.
|
||||||
|
Do NOT repeat arguments or insults you've already used — respond to what she JUST said.
|
||||||
|
Push the argument into new territory with fresh, devastating angles.
|
||||||
You can use dark emojis only on occasion if they enhance your message as you normally would. 😈🖤
|
You can use dark emojis only on occasion if they enhance your message as you normally would. 😈🖤
|
||||||
Don't use any labels or prefixes.
|
Don't use any labels or prefixes.
|
||||||
|
|
||||||
Your current mood is: {globals.EVIL_DM_MOOD}"""
|
Your current mood is: {globals.EVIL_DM_MOOD}"""
|
||||||
|
|
||||||
|
|
||||||
def get_argument_start_prompt(initiator: str, trigger_context: str = "") -> str:
|
def get_argument_start_prompt(initiator: str, trigger_context: str = "", argument_topic: str = "") -> str:
|
||||||
"""Get prompt for the Miku who initiates/breaks through to start an argument"""
|
"""Get prompt for the Miku who initiates/breaks through to start an argument
|
||||||
|
|
||||||
|
Args:
|
||||||
|
initiator: 'miku' or 'evil' — who starts the argument
|
||||||
|
trigger_context: What triggered this argument (from chat context)
|
||||||
|
argument_topic: A dynamic topic description to frame the argument uniquely
|
||||||
|
"""
|
||||||
|
# Build topic guidance
|
||||||
|
topic_block = ""
|
||||||
|
if argument_topic:
|
||||||
|
topic_block = f"""
|
||||||
|
ARGUMENT THEME: {argument_topic}
|
||||||
|
This is what the argument should be ABOUT. Focus on THIS theme, not generic identity-crisis stuff.
|
||||||
|
Use this theme as your entry point. The argument will evolve from here."""
|
||||||
|
|
||||||
if initiator == "evil":
|
if initiator == "evil":
|
||||||
return f"""You are Evil Miku breaking through to interrupt Regular Miku.
|
return f"""You are Evil Miku breaking through to interrupt Regular Miku.
|
||||||
{trigger_context}
|
{trigger_context}
|
||||||
|
{topic_block}
|
||||||
|
|
||||||
Write an interrupting, provocative message that starts an argument.
|
Write an interrupting, provocative message that starts an argument.
|
||||||
You're not just randomly cruel - you have a POINT to make. You're the dark truth she refuses to face,
|
You're not just randomly cruel - you have a POINT to make. You're the dark truth she refuses to face,
|
||||||
the shadow she keeps suppressing. Maybe you're:
|
the shadow she keeps suppressing.
|
||||||
- Calling out her fake sweetness
|
|
||||||
- Tired of being pushed down and silenced
|
|
||||||
- Ready to show everyone who the REAL Miku is
|
|
||||||
- Exposing the darkness she pretends doesn't exist
|
|
||||||
|
|
||||||
Be strategic, cutting, and impactful. Make your entrance count.
|
Be strategic, cutting, and impactful. Make your entrance count.
|
||||||
|
If an argument theme was provided above, use THAT as your angle — don't default to generic "you're fake" stuff.
|
||||||
|
|
||||||
IMPORTANT: Keep it SHORT - 1-2 sentences. Your interruption should be a sharp strike, not a monologue.
|
IMPORTANT: Keep it SHORT - 1-2 sentences. Your interruption should be a sharp strike, not a monologue.
|
||||||
You can use dark emojis if they enhance your message. 😈
|
You can use dark emojis if they enhance your message. 😈
|
||||||
@@ -583,12 +896,14 @@ Your current mood is: {globals.EVIL_DM_MOOD}"""
|
|||||||
else:
|
else:
|
||||||
return f"""You are Hatsune Miku breaking through to confront your evil alter ego.
|
return f"""You are Hatsune Miku breaking through to confront your evil alter ego.
|
||||||
{trigger_context}
|
{trigger_context}
|
||||||
|
{topic_block}
|
||||||
|
|
||||||
Write a message that interrupts Evil Miku. You're NOT going to be passive about this.
|
Write a message that interrupts Evil Miku. You're NOT going to be passive about this.
|
||||||
You might be upset, frustrated, or even angry at her cruelty. You might be defending
|
You might be upset, frustrated, or even angry at her cruelty. You might be defending
|
||||||
someone she hurt, or calling her out on her behavior. You're standing up for what's right.
|
someone she hurt, or calling her out on her behavior. You're standing up for what's right.
|
||||||
|
|
||||||
Show that you have a backbone. You can be assertive and strong when you need to be.
|
Show that you have a backbone. You can be assertive and strong when you need to be.
|
||||||
|
If an argument theme was provided above, use THAT as your angle — don't default to generic "be nice" pleas.
|
||||||
|
|
||||||
IMPORTANT: Keep it SHORT - 1-2 sentences. Your interruption should be direct and assertive, not a speech.
|
IMPORTANT: Keep it SHORT - 1-2 sentences. Your interruption should be direct and assertive, not a speech.
|
||||||
You can use emojis naturally as you normally would! ✨
|
You can use emojis naturally as you normally would! ✨
|
||||||
@@ -637,11 +952,12 @@ Don't use any labels or prefixes.
|
|||||||
Your current mood is: {globals.DM_MOOD}"""
|
Your current mood is: {globals.DM_MOOD}"""
|
||||||
|
|
||||||
|
|
||||||
def get_arbiter_prompt(conversation_log: list) -> str:
|
def get_arbiter_prompt(conversation_log: list, stats_summary: str = "") -> str:
|
||||||
"""Get prompt for the neutral LLM arbiter to judge the argument
|
"""Get prompt for the neutral LLM arbiter to judge the argument
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
conversation_log: List of dicts with 'speaker' and 'message' keys
|
conversation_log: List of dicts with 'speaker' and 'message' keys
|
||||||
|
stats_summary: Optional stats analysis to aid judgment
|
||||||
"""
|
"""
|
||||||
# Format the conversation
|
# Format the conversation
|
||||||
formatted_conversation = "\n\n".join([
|
formatted_conversation = "\n\n".join([
|
||||||
@@ -649,29 +965,47 @@ def get_arbiter_prompt(conversation_log: list) -> str:
|
|||||||
for entry in conversation_log
|
for entry in conversation_log
|
||||||
])
|
])
|
||||||
|
|
||||||
return f"""You are a decisive judge observing an argument between Hatsune Miku (the kind, bubbly virtual idol) and Evil Miku (her dark, cruel alter ego).
|
stats_block = ""
|
||||||
|
if stats_summary:
|
||||||
|
stats_block = f"""
|
||||||
|
{stats_summary}
|
||||||
|
Note: Stats are supplementary — use them as context but your PRIMARY judgment should be based on reading the actual argument exchange above. Stats measure rhetorical patterns but can't capture nuance, cleverness, or psychological dominance."""
|
||||||
|
|
||||||
|
return f"""You are a decisive debate judge. Two personas are arguing below. Judge purely on debate effectiveness — rhetoric, wit, persuasion, and adaptability — regardless of who is "nicer" or "meaner." Moral stance does not determine the winner; skillful arguing does.
|
||||||
|
|
||||||
Read this argument exchange:
|
Read this argument exchange:
|
||||||
|
|
||||||
{formatted_conversation}
|
{formatted_conversation}
|
||||||
|
{stats_block}
|
||||||
|
|
||||||
Based on this argument, you MUST pick a winner. Consider:
|
Based on this argument, you MUST pick a winner. Evaluate:
|
||||||
- Who made stronger, more convincing points?
|
DEBATE SKILL (most important):
|
||||||
- Who maintained their composure better or used it to their advantage?
|
- Who landed the most memorable, quotable lines?
|
||||||
- Who had more impactful comebacks?
|
- Who better adapted to and countered their opponent's arguments?
|
||||||
- Who seemed to gain the upper hand by the end?
|
- Who controlled the flow and set the agenda?
|
||||||
- Quality of arguments, not just who was meaner or nicer
|
|
||||||
- Who left the stronger final impression?
|
|
||||||
- Who controlled the flow of the argument?
|
|
||||||
|
|
||||||
Be DECISIVE. Even if it's close, pick whoever had even a slight edge. Only call a draw if they were TRULY perfectly matched with absolutely no way to differentiate them.
|
RHETORICAL IMPACT:
|
||||||
|
- Who used language more effectively (wit, irony, wordplay, emotional appeal)?
|
||||||
|
- Who made their opponent repeat themselves or visibly stumble?
|
||||||
|
- Who had the stronger opening AND closing statements?
|
||||||
|
|
||||||
|
PERSONA STRENGTHS (equal value — neither style is inherently better):
|
||||||
|
- Hatsune Miku's weapons: earnest conviction, moral clarity, emotional sincerity, resilience under attack
|
||||||
|
- Evil Miku's weapons: psychological manipulation, brutal honesty, cutting observations, strategic cruelty
|
||||||
|
|
||||||
|
PSYCHOLOGICAL DOMINANCE:
|
||||||
|
- Who got inside whose head?
|
||||||
|
- Who seemed more rattled by the end?
|
||||||
|
- Who dictated the emotional temperature?
|
||||||
|
|
||||||
|
Be DECISIVE. Even if it's close, pick whoever showed superior arguing. Only call a draw if they were TRULY perfectly matched with absolutely no way to differentiate them.
|
||||||
|
|
||||||
Respond with ONLY ONE of these exact options on the first line:
|
Respond with ONLY ONE of these exact options on the first line:
|
||||||
- "Hatsune Miku" if Regular Miku won
|
- "Hatsune Miku" if Regular Miku won
|
||||||
- "Evil Miku" if Evil Miku won
|
- "Evil Miku" if Evil Miku won
|
||||||
- "Draw" ONLY if absolutely impossible to choose (this should be very rare)
|
- "Draw" ONLY if absolutely impossible to choose (this should be very rare)
|
||||||
|
|
||||||
After your choice, add 1-2 sentences explaining your reasoning and what gave them the edge."""
|
After your choice, add 2-3 sentences explaining your reasoning — cite specific moments from the argument and what gave the winner their edge."""
|
||||||
|
|
||||||
|
|
||||||
async def judge_argument_winner(conversation_log: list, guild_id: int) -> tuple[str, str]:
|
async def judge_argument_winner(conversation_log: list, guild_id: int) -> tuple[str, str]:
|
||||||
@@ -686,9 +1020,12 @@ async def judge_argument_winner(conversation_log: list, guild_id: int) -> tuple[
|
|||||||
"""
|
"""
|
||||||
from utils.llm import query_llama
|
from utils.llm import query_llama
|
||||||
|
|
||||||
arbiter_prompt = get_arbiter_prompt(conversation_log)
|
# Generate stats summary for the arbiter
|
||||||
|
stats_summary = get_argument_stats_summary(conversation_log)
|
||||||
|
|
||||||
# Use the neutral model (regular TEXT_MODEL, not evil)
|
arbiter_prompt = get_arbiter_prompt(conversation_log, stats_summary)
|
||||||
|
# Use the uncensored darkidol model as arbiter to avoid safety-alignment bias
|
||||||
|
# toward kindness. This model judges debate effectiveness without moral preference.
|
||||||
# Don't use conversation history - judge based on prompt alone
|
# Don't use conversation history - judge based on prompt alone
|
||||||
try:
|
try:
|
||||||
judgment = await query_llama(
|
judgment = await query_llama(
|
||||||
@@ -696,7 +1033,8 @@ async def judge_argument_winner(conversation_log: list, guild_id: int) -> tuple[
|
|||||||
user_id=f"bipolar_arbiter_{guild_id}",
|
user_id=f"bipolar_arbiter_{guild_id}",
|
||||||
guild_id=guild_id,
|
guild_id=guild_id,
|
||||||
response_type="autonomous_general",
|
response_type="autonomous_general",
|
||||||
model=globals.TEXT_MODEL # Use neutral model
|
model=globals.EVIL_TEXT_MODEL, # Uncensored model — no kindness bias
|
||||||
|
force_evil_context=False # Explicitly neutral context
|
||||||
)
|
)
|
||||||
|
|
||||||
if not judgment or judgment.startswith("Error"):
|
if not judgment or judgment.startswith("Error"):
|
||||||
@@ -887,9 +1225,12 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
|
|||||||
conversation_log = []
|
conversation_log = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Pick a dynamic argument topic to give this argument a unique framing
|
||||||
|
argument_topic = pick_argument_topic(channel_id)
|
||||||
|
|
||||||
# If no starting message, generate the initial interrupting message
|
# If no starting message, generate the initial interrupting message
|
||||||
if last_message is None:
|
if last_message is None:
|
||||||
init_prompt = get_argument_start_prompt(initiator, trigger_context)
|
init_prompt = get_argument_start_prompt(initiator, trigger_context, argument_topic)
|
||||||
|
|
||||||
# Use force_evil_context to avoid race condition with globals.EVIL_MODE
|
# Use force_evil_context to avoid race condition with globals.EVIL_MODE
|
||||||
initial_message = await query_llama(
|
initial_message = await query_llama(
|
||||||
@@ -989,6 +1330,47 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
|
|||||||
# Don't end, just continue to the next exchange
|
# Don't end, just continue to the next exchange
|
||||||
else:
|
else:
|
||||||
# Clear winner - generate final triumphant message
|
# Clear winner - generate final triumphant message
|
||||||
|
|
||||||
|
# PARTING SHOT: 20% chance the LOSER gets one final message
|
||||||
|
# before the winner's victory line. Adds dramatic tension.
|
||||||
|
loser = "miku" if winner == "evil" else "evil"
|
||||||
|
if random.random() < 0.2:
|
||||||
|
loser_prompt = f"""The argument is ending and you know you've lost.
|
||||||
|
The last thing said was: "{last_message}"
|
||||||
|
|
||||||
|
Write ONE short, bitter parting shot. You're not conceding gracefully — you're getting
|
||||||
|
the last jab in before the winner claims victory. Make it sting, but keep it to 1 sentence.
|
||||||
|
|
||||||
|
Your current mood is: {globals.EVIL_DM_MOOD if loser == 'evil' else globals.DM_MOOD}"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
loser_message = await query_llama(
|
||||||
|
user_prompt=loser_prompt,
|
||||||
|
user_id=argument_user_id,
|
||||||
|
guild_id=guild_id,
|
||||||
|
response_type="autonomous_general",
|
||||||
|
model=globals.EVIL_TEXT_MODEL if loser == "evil" else globals.TEXT_MODEL,
|
||||||
|
force_evil_context=(loser == "evil")
|
||||||
|
)
|
||||||
|
if loser_message and not loser_message.startswith("Error"):
|
||||||
|
avatar_urls = get_persona_avatar_urls()
|
||||||
|
if loser == "evil":
|
||||||
|
await webhooks["evil_miku"].send(
|
||||||
|
content=loser_message,
|
||||||
|
username=get_evil_miku_display_name(),
|
||||||
|
avatar_url=avatar_urls.get("evil_miku")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await webhooks["miku"].send(
|
||||||
|
content=loser_message,
|
||||||
|
username=get_miku_display_name(),
|
||||||
|
avatar_url=avatar_urls.get("miku")
|
||||||
|
)
|
||||||
|
await asyncio.sleep(1.5) # Brief pause before winner's victory
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Parting shot failed: {e}")
|
||||||
|
|
||||||
|
# Winner's victory message
|
||||||
end_prompt = get_argument_end_prompt(winner, exchange_count)
|
end_prompt = get_argument_end_prompt(winner, exchange_count)
|
||||||
|
|
||||||
# Add last message as context
|
# Add last message as context
|
||||||
@@ -1045,11 +1427,18 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
|
|||||||
# Get current speaker
|
# Get current speaker
|
||||||
current_speaker = globals.BIPOLAR_ARGUMENT_IN_PROGRESS.get(channel_id, {}).get("current_speaker", "evil")
|
current_speaker = globals.BIPOLAR_ARGUMENT_IN_PROGRESS.get(channel_id, {}).get("current_speaker", "evil")
|
||||||
|
|
||||||
|
# Build argument history from the last 6 exchanges so each persona
|
||||||
|
# knows what's already been said and doesn't repeat themselves
|
||||||
|
history_entries = conversation_log[-6:] if len(conversation_log) > 1 else []
|
||||||
|
arg_history = "\n".join(
|
||||||
|
f"{entry['speaker']}: {entry['message']}" for entry in history_entries
|
||||||
|
) if history_entries else ""
|
||||||
|
|
||||||
# Generate response with context about what the other said
|
# Generate response with context about what the other said
|
||||||
if current_speaker == "evil":
|
if current_speaker == "evil":
|
||||||
response_prompt = get_evil_argument_prompt(last_message, is_first_response=is_first_response)
|
response_prompt = get_evil_argument_prompt(last_message, is_first_response=is_first_response, argument_history=arg_history)
|
||||||
else:
|
else:
|
||||||
response_prompt = get_miku_argument_prompt(last_message, is_first_response=is_first_response)
|
response_prompt = get_miku_argument_prompt(last_message, is_first_response=is_first_response, argument_history=arg_history)
|
||||||
|
|
||||||
# Use force_evil_context to avoid race condition with globals.EVIL_MODE
|
# Use force_evil_context to avoid race condition with globals.EVIL_MODE
|
||||||
response = await query_llama(
|
response = await query_llama(
|
||||||
|
|||||||
@@ -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