diff --git a/bot/utils/bipolar_mode.py b/bot/utils/bipolar_mode.py index 7a1d7c1..34ccfe9 100644 --- a/bot/utils/bipolar_mode.py +++ b/bot/utils/bipolar_mode.py @@ -883,23 +883,15 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st if last_message is None: init_prompt = get_argument_start_prompt(initiator, trigger_context) - # Temporarily set evil mode for query_llama if initiator is evil - original_evil_mode = globals.EVIL_MODE - if initiator == "evil": - globals.EVIL_MODE = True - else: - globals.EVIL_MODE = False - - try: - initial_message = await query_llama( - user_prompt=init_prompt, - user_id=argument_user_id, - guild_id=guild_id, - response_type="autonomous_general", - model=globals.EVIL_TEXT_MODEL if initiator == "evil" else globals.TEXT_MODEL - ) - finally: - globals.EVIL_MODE = original_evil_mode + # Use force_evil_context to avoid race condition with globals.EVIL_MODE + initial_message = await query_llama( + user_prompt=init_prompt, + user_id=argument_user_id, + guild_id=guild_id, + response_type="autonomous_general", + model=globals.EVIL_TEXT_MODEL if initiator == "evil" else globals.TEXT_MODEL, + force_evil_context=(initiator == "evil") + ) if not initial_message or initial_message.startswith("Error") or initial_message.startswith("Sorry"): logger.error("Failed to generate initial argument message") @@ -994,23 +986,15 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st # Add last message as context response_prompt = f'The other Miku said: "{last_message}"\n\n{end_prompt}' - # Temporarily set evil mode for query_llama - original_evil_mode = globals.EVIL_MODE - if winner == "evil": - globals.EVIL_MODE = True - else: - globals.EVIL_MODE = False - - try: - final_message = await query_llama( - user_prompt=response_prompt, - user_id=argument_user_id, - guild_id=guild_id, - response_type="autonomous_general", - model=globals.EVIL_TEXT_MODEL if winner == "evil" else globals.TEXT_MODEL - ) - finally: - globals.EVIL_MODE = original_evil_mode + # Use force_evil_context to avoid race condition with globals.EVIL_MODE + final_message = await query_llama( + user_prompt=response_prompt, + user_id=argument_user_id, + guild_id=guild_id, + response_type="autonomous_general", + model=globals.EVIL_TEXT_MODEL if winner == "evil" else globals.TEXT_MODEL, + force_evil_context=(winner == "evil") + ) if final_message and not final_message.startswith("Error") and not final_message.startswith("Sorry"): # Send winner's final message via webhook @@ -1059,23 +1043,15 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st else: response_prompt = get_miku_argument_prompt(last_message, is_first_response=is_first_response) - # Temporarily set evil mode for query_llama - original_evil_mode = globals.EVIL_MODE - if current_speaker == "evil": - globals.EVIL_MODE = True - else: - globals.EVIL_MODE = False - - try: - response = await query_llama( - user_prompt=response_prompt, - user_id=argument_user_id, - guild_id=guild_id, - response_type="autonomous_general", - model=globals.EVIL_TEXT_MODEL if current_speaker == "evil" else globals.TEXT_MODEL - ) - finally: - globals.EVIL_MODE = original_evil_mode + # Use force_evil_context to avoid race condition with globals.EVIL_MODE + response = await query_llama( + user_prompt=response_prompt, + user_id=argument_user_id, + guild_id=guild_id, + response_type="autonomous_general", + model=globals.EVIL_TEXT_MODEL if current_speaker == "evil" else globals.TEXT_MODEL, + force_evil_context=(current_speaker == "evil") + ) if not response or response.startswith("Error") or response.startswith("Sorry"): logger.error(f"Failed to generate argument response") diff --git a/bot/utils/moods.py b/bot/utils/moods.py index 2fae966..19c17f5 100644 --- a/bot/utils/moods.py +++ b/bot/utils/moods.py @@ -33,7 +33,13 @@ EVIL_MOOD_EMOJIS = { "aggressive": "👿", "cunning": "🐍", "sarcastic": "😈", - "evil_neutral": "" + "evil_neutral": "", + "bored": "🥱", + "manic": "🤪", + "jealous": "💚", + "melancholic": "🌑", + "playful_cruel": "🎭", + "contemptuous": "👑" } def load_mood_description(mood_name: str) -> str: @@ -150,33 +156,32 @@ def detect_mood_shift(response_text, server_context=None): return None async def rotate_dm_mood(): - """Rotate DM mood automatically (handles both normal and evil mode)""" + """Rotate DM mood automatically (normal mode only — evil has its own independent timer)""" try: - from utils.evil_mode import is_evil_mode, rotate_evil_mood + from utils.evil_mode import is_evil_mode if is_evil_mode(): - # Rotate evil mood instead - await rotate_evil_mood() - else: - # Normal mood rotation - old_mood = globals.DM_MOOD - new_mood = old_mood - attempts = 0 - # Filter out 'asleep' — DMs have no sleepy→asleep transition guard - dm_eligible = [m for m in globals.AVAILABLE_MOODS if m != "asleep"] - - while new_mood == old_mood and attempts < 5: - new_mood = random.choice(dm_eligible) - attempts += 1 - - globals.DM_MOOD = new_mood - globals.DM_MOOD_DESCRIPTION = load_mood_description(new_mood) - - logger.info(f"DM mood rotated from {old_mood} to {new_mood}") + # Evil mode has its own independent 2-hour rotation timer in evil_mode.py + # Do nothing here — evil mood rotation is handled by start_evil_mood_rotation() + logger.debug("Skipping DM mood rotation — evil mode has its own timer") + return - # Note: We don't update server nicknames here because servers have their own independent moods. - # DM mood only affects direct messages to users. + # Normal mood rotation + old_mood = globals.DM_MOOD + new_mood = old_mood + attempts = 0 + # Filter out 'asleep' — DMs have no sleepy→asleep transition guard + dm_eligible = [m for m in globals.AVAILABLE_MOODS if m != "asleep"] + while new_mood == old_mood and attempts < 5: + new_mood = random.choice(dm_eligible) + attempts += 1 + + globals.DM_MOOD = new_mood + globals.DM_MOOD_DESCRIPTION = load_mood_description(new_mood) + + logger.info(f"DM mood rotated from {old_mood} to {new_mood}") + except Exception as e: logger.error(f"Exception in rotate_dm_mood: {e}") diff --git a/bot/utils/persona_dialogue.py b/bot/utils/persona_dialogue.py index 20364c0..974e8df 100644 --- a/bot/utils/persona_dialogue.py +++ b/bot/utils/persona_dialogue.py @@ -264,12 +264,15 @@ class InterjectionScorer: if opposite_persona == "evil": MOOD_MULTIPLIERS = { "aggressive": 1.5, - "cruel": 1.3, - "mischievous": 1.2, + "manic": 1.4, + "jealous": 1.3, "cunning": 1.0, "sarcastic": 1.1, + "playful_cruel": 1.2, + "contemptuous": 0.7, "evil_neutral": 0.8, - "contemplative": 0.6, + "bored": 0.5, + "melancholic": 0.6, } return MOOD_MULTIPLIERS.get(globals.EVIL_DM_MOOD, 1.0) else: @@ -505,20 +508,15 @@ On a new line after your response, write: # Use appropriate model model = globals.EVIL_TEXT_MODEL if responding_persona == "evil" else globals.TEXT_MODEL - # Temporarily set evil mode for proper context - original_evil_mode = globals.EVIL_MODE - globals.EVIL_MODE = (responding_persona == "evil") - - try: - raw_response = await query_llama( - user_prompt=prompt, - user_id=f"persona_dialogue_{channel.id}", - guild_id=channel.guild.id if hasattr(channel, 'guild') and channel.guild else None, - response_type="autonomous_general", - model=model - ) - finally: - globals.EVIL_MODE = original_evil_mode + # Use force_evil_context to avoid race condition with globals.EVIL_MODE + raw_response = await query_llama( + user_prompt=prompt, + user_id=f"persona_dialogue_{channel.id}", + guild_id=channel.guild.id if hasattr(channel, 'guild') and channel.guild else None, + response_type="autonomous_general", + model=model, + force_evil_context=(responding_persona == "evil") + ) if not raw_response or raw_response.startswith("Error"): return None, False, "LOW" @@ -553,10 +551,12 @@ On a new line after your response, write: response_text = '\n'.join(response_lines).strip() - # Clean up any stray signal markers - response_text = response_text.replace("[CONTINUE:", "").replace("]", "") - response_text = response_text.replace("YES", "").replace("NO", "") - response_text = response_text.replace("HIGH", "").replace("MEDIUM", "").replace("LOW", "") + # Clean up any stray [CONTINUE: markers that leaked into response lines + # Only strip the structural markers, NOT common words like YES/NO/HIGH etc. + import re + response_text = re.sub(r'\[CONTINUE:\s*(YES|NO)\]\s*\[CONFIDENCE:\s*(HIGH|MEDIUM|LOW)\]', '', response_text) + response_text = re.sub(r'\[CONTINUE:\s*(YES|NO)\]', '', response_text) + response_text = re.sub(r'\[CONFIDENCE:\s*(HIGH|MEDIUM|LOW)\]', '', response_text) response_text = response_text.strip() # Override: If the response contains a question mark, always continue @@ -727,19 +727,15 @@ Don't force a response if you have nothing meaningful to contribute.""" model = globals.EVIL_TEXT_MODEL if persona == "evil" else globals.TEXT_MODEL - original_evil_mode = globals.EVIL_MODE - globals.EVIL_MODE = (persona == "evil") - - try: - response = await query_llama( - user_prompt=prompt, - user_id=f"persona_dialogue_{channel_id}", - guild_id=channel.guild.id if hasattr(channel, 'guild') and channel.guild else None, - response_type="autonomous_general", - model=model - ) - finally: - globals.EVIL_MODE = original_evil_mode + # Use force_evil_context to avoid race condition with globals.EVIL_MODE + response = await query_llama( + user_prompt=prompt, + user_id=f"persona_dialogue_{channel_id}", + guild_id=channel.guild.id if hasattr(channel, 'guild') and channel.guild else None, + response_type="autonomous_general", + model=model, + force_evil_context=(persona == "evil") + ) if not response: self.end_dialogue(channel_id) diff --git a/cat-plugins/profile_picture_context/plugin.json b/cat-plugins/profile_picture_context/plugin.json new file mode 100644 index 0000000..8ae3218 --- /dev/null +++ b/cat-plugins/profile_picture_context/plugin.json @@ -0,0 +1,10 @@ +{ + "name": "Profile Picture Context", + "description": "Injects profile picture description only when user asks about it using regex pattern matching", + "author_name": "Miku Bot Team", + "author_url": "", + "plugin_url": "", + "tags": "profile, picture, context, regex", + "thumb": "", + "version": "1.0.0" +} diff --git a/cat-plugins/profile_picture_context/profile_picture_context.py b/cat-plugins/profile_picture_context/profile_picture_context.py index a927a61..c15bbb5 100644 --- a/cat-plugins/profile_picture_context/profile_picture_context.py +++ b/cat-plugins/profile_picture_context/profile_picture_context.py @@ -14,12 +14,31 @@ import re # Regex patterns that match profile picture questions PFP_PATTERNS = [ - r'\b(what|describe|tell me about|explain)\b.*\b(pfp|profile pic|avatar|picture)\b', - r'\b(your|miku\'?s?)\b.*\b(pfp|profile pic|avatar|picture)\b', - r'\bwhat.*looking like\b', + # Direct PFP references + r'\b(what|describe|tell me about|explain|show|how)\b.*\b(pfp|profile pic|avatar|picture|pic)\b', + r'\b(your|miku\'?s?)\b.*\b(pfp|profile pic|avatar|picture|pic)\b', + r'\b(pfp|profile pic|avatar|picture|pic)\b.*\b(is|look|show|about|like)', + + # Questions about appearance + r'\b(what|how).*\b(you|miku)\b.*(look|looking|appear)', r'\byour (new )?look\b', - r'\bhow.*look(ing)?\b.*today', - r'\b(pfp|profile pic|avatar)\b.*\b(is|look|show)', + r'\b(what|how).*looking like\b', + + # Questions about the image itself + r'\b(think|feel|opinion|thoughts)\b.*\b(about|of)\b.*\b(your|that|the|this)?\b.*\b(pfp|profile|avatar|pic|picture|image)\b', + r'\b(why|how|when).*\b(pick|choose|chose|picked|select|change|changed)\b.*\b(pfp|profile|avatar|pic|picture|that)\b', + r'\b(new|current|latest)\b.*\b(pfp|profile pic|avatar|pic|picture)\b', + + # "What do you think about your pfp" + r'\bthink.*\b(your|that|the|this)\b.*\b(pfp|profile|avatar|pic|picture)\b', + r'\b(your|that|the|this)\b.*\b(pfp|profile|avatar|pic|picture)\b.*\bthink\b', + + # "How did you decide/pick" + r'\b(decide|decided|pick|picked|choose|chose|select)\b.*\b(pfp|profile|avatar|pic|picture|that|this)\b', + + # "Tell me about that pfp" / "What's with the pfp" + r'\bwhat\'?s?\b.*\bwith\b.*\b(pfp|profile|avatar|pic|picture)\b', + r'\btell me\b.*\b(pfp|profile|avatar|pic|picture|that|this)\b', ] def matches_pfp_query(text: str) -> bool: