Files
Breehavior-Monitor/cogs/chat.py
AJ Isaacs e41845de02 Add scoreboard roast feature via image analysis
When @mentioned with an image attachment, the bot now roasts players
based on scoreboard screenshots using the vision model. Text-only
mentions continue to work as before.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 16:30:26 -05:00

139 lines
4.6 KiB
Python

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))