diff --git a/cogs/chat.py b/cogs/chat.py index 2ba70eb..517b5b8 100644 --- a/cogs/chat.py +++ b/cogs/chat.py @@ -273,11 +273,14 @@ class ChatCog(commands.Cog): image_attachment.filename, user_text[:80], ) + ext = image_attachment.filename.rsplit(".", 1)[-1].lower() if "." in image_attachment.filename else "png" + mime = f"image/{'jpeg' if ext == 'jpg' else ext}" response = await self.bot.llm_heavy.analyze_image( image_bytes, IMAGE_ROAST, user_text=user_text, on_first_token=start_typing, + media_type=mime, ) else: # --- Text-only path: normal chat --- diff --git a/cogs/commands.py b/cogs/commands.py index 70e0fca..a4572e1 100644 --- a/cogs/commands.py +++ b/cogs/commands.py @@ -320,9 +320,8 @@ class CommandsCog(commands.Cog): f"Scanning {len(messages)} messages... (first request may be slow while model loads)" ) - for msg in messages: + for idx, msg in enumerate(messages): # Build context from the messages before this one - idx = messages.index(msg) ctx_msgs = messages[max(0, idx - 3):idx] context = ( " | ".join(f"{m.author.display_name}: {m.content}" for m in ctx_msgs) diff --git a/cogs/sentiment/__init__.py b/cogs/sentiment/__init__.py index 7746c4d..d0f039a 100644 --- a/cogs/sentiment/__init__.py +++ b/cogs/sentiment/__init__.py @@ -145,7 +145,9 @@ class SentimentCog(commands.Cog): mention_config = config.get("mention_scan", {}) if mention_config.get("enabled", True): await self._maybe_start_mention_scan(message, mention_config) - return + return + # For non-report intents, fall through to buffer the message + # so it still gets scored for toxicity # Skip if empty if not message.content or not message.content.strip(): @@ -317,11 +319,13 @@ class SentimentCog(commands.Cog): if aliases: anon_key = anon_map.get(msg.author.display_name, msg.author.display_name) lines.append(f" {anon_key} is also known as: {', '.join(aliases)}") - # Also include aliases for members NOT in the conversation (so the LLM - # can recognize name-drops of absent members) + # Include aliases for members NOT in the conversation (so the LLM + # can recognize name-drops of absent members), using anonymized keys + absent_idx = 0 for uid, aliases in all_aliases.items(): if uid not in seen_ids: - lines.append(f" (not in chat) also known as: {', '.join(aliases)}") + absent_idx += 1 + lines.append(f" Absent_{absent_idx} is also known as: {', '.join(aliases)}") return "\n".join(lines) if lines else "" @staticmethod diff --git a/cogs/sentiment/state.py b/cogs/sentiment/state.py index 35a6e5b..921af24 100644 --- a/cogs/sentiment/state.py +++ b/cogs/sentiment/state.py @@ -32,19 +32,24 @@ async def flush_dirty_states(bot, dirty_users: set[int]) -> None: if not dirty_users: return dirty = list(dirty_users) - dirty_users.clear() + saved = 0 for user_id in dirty: user_data = bot.drama_tracker.get_user(user_id) - await bot.db.save_user_state( - user_id=user_id, - offense_count=user_data.offense_count, - immune=user_data.immune, - off_topic_count=user_data.off_topic_count, - baseline_coherence=user_data.baseline_coherence, - user_notes=user_data.notes or None, - warned=user_data.warned_since_reset, - last_offense_at=user_data.last_offense_time or None, - aliases=_aliases_csv(user_data), - warning_expires_at=user_data.warning_expires_at or None, - ) - logger.info("Flushed %d dirty user states to DB.", len(dirty)) + try: + await bot.db.save_user_state( + user_id=user_id, + offense_count=user_data.offense_count, + immune=user_data.immune, + off_topic_count=user_data.off_topic_count, + baseline_coherence=user_data.baseline_coherence, + user_notes=user_data.notes or None, + warned=user_data.warned_since_reset, + last_offense_at=user_data.last_offense_time or None, + aliases=_aliases_csv(user_data), + warning_expires_at=user_data.warning_expires_at or None, + ) + dirty_users.discard(user_id) + saved += 1 + except Exception: + logger.exception("Failed to flush state for user %d", user_id) + logger.info("Flushed %d/%d dirty user states to DB.", saved, len(dirty)) diff --git a/utils/database.py b/utils/database.py index b121f5e..16079ef 100644 --- a/utils/database.py +++ b/utils/database.py @@ -628,7 +628,8 @@ class Database: return [] # Build OR conditions for each keyword conditions = " OR ".join(["Topics LIKE ?" for _ in topic_keywords]) - params = [limit, user_id] + [f"%{kw}%" for kw in topic_keywords] + escaped = [kw.replace("%", "[%]").replace("_", "[_]") for kw in topic_keywords] + params = [limit, user_id] + [f"%{kw}%" for kw in escaped] cursor.execute( f"""SELECT TOP (?) Memory, Topics, Importance, CreatedAt FROM UserMemory diff --git a/utils/llm_client.py b/utils/llm_client.py index 0e03c69..ca4bc3d 100644 --- a/utils/llm_client.py +++ b/utils/llm_client.py @@ -865,13 +865,14 @@ class LLMClient: system_prompt: str, user_text: str = "", on_first_token=None, + media_type: str = "image/png", ) -> str | None: """Send an image to the vision model with a system prompt. Returns the generated text response, or None on failure. """ b64 = base64.b64encode(image_bytes).decode() - data_url = f"data:image/png;base64,{b64}" + data_url = f"data:{media_type};base64,{b64}" user_content: list[dict] = [ {"type": "image_url", "image_url": {"url": data_url}},