Add switchable bot modes: default, chatty, and roast
Adds a server-wide mode system with /bcs-mode command. - Default: current hall-monitor behavior unchanged - Chatty: friendly chat participant with proactive replies (~10% chance) - Roast: savage roast mode with proactive replies - Chatty/roast use relaxed moderation thresholds - 5-message cooldown between proactive replies per channel - Bot status updates to reflect active mode - /bcs-status shows current mode and effective thresholds Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
58
cogs/chat.py
58
cogs/chat.py
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
|
||||
@@ -9,17 +10,33 @@ from discord.ext import commands
|
||||
logger = logging.getLogger("bcs.chat")
|
||||
|
||||
_PROMPTS_DIR = Path(__file__).resolve().parent.parent / "prompts"
|
||||
CHAT_PERSONALITY = (_PROMPTS_DIR / "chat_personality.txt").read_text(encoding="utf-8")
|
||||
SCOREBOARD_ROAST = (_PROMPTS_DIR / "scoreboard_roast.txt").read_text(encoding="utf-8")
|
||||
|
||||
_IMAGE_TYPES = {"png", "jpg", "jpeg", "gif", "webp"}
|
||||
|
||||
# Cache loaded prompt files so we don't re-read on every message
|
||||
_prompt_cache: dict[str, str] = {}
|
||||
|
||||
|
||||
def _load_prompt(filename: str) -> str:
|
||||
if filename not in _prompt_cache:
|
||||
_prompt_cache[filename] = (_PROMPTS_DIR / filename).read_text(encoding="utf-8")
|
||||
return _prompt_cache[filename]
|
||||
|
||||
|
||||
class ChatCog(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
# Per-channel conversation history for the bot: {channel_id: deque of {role, content}}
|
||||
self._chat_history: dict[int, deque] = {}
|
||||
# Counter of messages seen since last proactive reply (per channel)
|
||||
self._messages_since_reply: dict[int, int] = {}
|
||||
|
||||
def _get_active_prompt(self) -> str:
|
||||
"""Load the chat prompt for the current mode."""
|
||||
mode_config = self.bot.get_mode_config()
|
||||
prompt_file = mode_config.get("prompt_file", "chat_personality.txt")
|
||||
return _load_prompt(prompt_file)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message(self, message: discord.Message):
|
||||
@@ -30,6 +47,7 @@ class ChatCog(commands.Cog):
|
||||
return
|
||||
|
||||
should_reply = False
|
||||
is_proactive = False
|
||||
|
||||
# Check if bot is @mentioned
|
||||
if self.bot.user in message.mentions:
|
||||
@@ -48,6 +66,24 @@ class ChatCog(commands.Cog):
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
|
||||
# Proactive reply check (only if not already replying to a mention/reply)
|
||||
if not should_reply:
|
||||
mode_config = self.bot.get_mode_config()
|
||||
if mode_config.get("proactive_replies", False):
|
||||
ch_id = message.channel.id
|
||||
self._messages_since_reply[ch_id] = self._messages_since_reply.get(ch_id, 0) + 1
|
||||
cooldown = self.bot.config.get("modes", {}).get("proactive_cooldown_messages", 5)
|
||||
reply_chance = mode_config.get("reply_chance", 0.0)
|
||||
|
||||
if (
|
||||
self._messages_since_reply[ch_id] >= cooldown
|
||||
and reply_chance > 0
|
||||
and random.random() < reply_chance
|
||||
and message.content and message.content.strip()
|
||||
):
|
||||
should_reply = True
|
||||
is_proactive = True
|
||||
|
||||
if not should_reply:
|
||||
return
|
||||
|
||||
@@ -94,7 +130,7 @@ class ChatCog(commands.Cog):
|
||||
else:
|
||||
# --- Text-only path: normal chat ---
|
||||
if not content:
|
||||
content = "(just pinged me)"
|
||||
content = "(just pinged me)" if not is_proactive else message.content
|
||||
|
||||
# Add drama score context only when noteworthy
|
||||
drama_score = self.bot.drama_tracker.get_drama_score(message.author.id)
|
||||
@@ -110,9 +146,11 @@ class ChatCog(commands.Cog):
|
||||
{"role": "user", "content": f"{score_context}\n{message.author.display_name}: {content}"}
|
||||
)
|
||||
|
||||
active_prompt = self._get_active_prompt()
|
||||
|
||||
response = await self.bot.llm.chat(
|
||||
list(self._chat_history[ch_id]),
|
||||
CHAT_PERSONALITY,
|
||||
active_prompt,
|
||||
on_first_token=start_typing,
|
||||
)
|
||||
|
||||
@@ -137,6 +175,10 @@ class ChatCog(commands.Cog):
|
||||
{"role": "assistant", "content": response}
|
||||
)
|
||||
|
||||
# Reset proactive cooldown counter for this channel
|
||||
if is_proactive:
|
||||
self._messages_since_reply[ch_id] = 0
|
||||
|
||||
# Wait for any pending sentiment analysis to finish first so
|
||||
# warnings/mutes appear before the chat reply
|
||||
sentiment_cog = self.bot.get_cog("SentimentCog")
|
||||
@@ -149,9 +191,15 @@ class ChatCog(commands.Cog):
|
||||
except (asyncio.TimeoutError, asyncio.CancelledError):
|
||||
pass
|
||||
|
||||
await message.reply(response, mention_author=False)
|
||||
if is_proactive:
|
||||
await message.channel.send(response)
|
||||
else:
|
||||
await message.reply(response, mention_author=False)
|
||||
|
||||
reply_type = "proactive" if is_proactive else "chat"
|
||||
logger.info(
|
||||
"Chat reply in #%s to %s: %s",
|
||||
"%s reply in #%s to %s: %s",
|
||||
reply_type.capitalize(),
|
||||
message.channel.name,
|
||||
message.author.display_name,
|
||||
response[:100],
|
||||
|
||||
Reference in New Issue
Block a user