import logging from collections import deque from pathlib import Path import discord 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"} 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] = {} @commands.Cog.listener() async def on_message(self, message: discord.Message): if message.author.bot: return if not message.guild: return should_reply = False # Check if bot is @mentioned if self.bot.user in message.mentions: should_reply = True # Check if replying to one of the bot's messages if message.reference and message.reference.message_id: try: ref_msg = message.reference.cached_message if ref_msg is None: ref_msg = await message.channel.fetch_message( message.reference.message_id ) if ref_msg.author.id == self.bot.user.id: should_reply = True except discord.HTTPException: pass if not should_reply: return # Build conversation context ch_id = message.channel.id if ch_id not in self._chat_history: self._chat_history[ch_id] = deque(maxlen=10) # Clean the mention out of the message content content = message.content.replace(f"<@{self.bot.user.id}>", "").strip() # Check for image attachments image_attachment = None for att in message.attachments: ext = att.filename.rsplit(".", 1)[-1].lower() if "." in att.filename else "" if ext in _IMAGE_TYPES: image_attachment = att break typing_ctx = None async def start_typing(): nonlocal typing_ctx typing_ctx = message.channel.typing() await typing_ctx.__aenter__() if image_attachment: # --- Image path: scoreboard roast --- image_bytes = await image_attachment.read() user_text = content if content else "Roast this scoreboard." logger.info( "Image roast request in #%s from %s (%s, %s)", message.channel.name, message.author.display_name, image_attachment.filename, user_text[:80], ) response = await self.bot.llm.analyze_image( image_bytes, SCOREBOARD_ROAST, user_text=user_text, on_first_token=start_typing, ) else: # --- Text-only path: normal chat --- if not content: content = "(just pinged me)" # Add drama score context to the user message drama_score = self.bot.drama_tracker.get_drama_score(message.author.id) user_data = self.bot.drama_tracker.get_user(message.author.id) score_context = ( f"[Server context: {message.author.display_name} has a drama score of " f"{drama_score:.2f}/1.0 and {user_data.offense_count} offenses. " f"They are talking in #{message.channel.name}.]" ) self._chat_history[ch_id].append( {"role": "user", "content": f"{score_context}\n{message.author.display_name}: {content}"} ) response = await self.bot.llm.chat( list(self._chat_history[ch_id]), CHAT_PERSONALITY, on_first_token=start_typing, ) if typing_ctx: await typing_ctx.__aexit__(None, None, None) if response is None: response = "I'd roast you but my brain is offline. Try again later." if not image_attachment: self._chat_history[ch_id].append( {"role": "assistant", "content": response} ) await message.reply(response, mention_author=False) logger.info( "Chat reply in #%s to %s: %s", message.channel.name, message.author.display_name, response[:100], ) async def setup(bot: commands.Bot): await bot.add_cog(ChatCog(bot))