From e488b2b227c84d6dfffeff85fe33994e1e170d6e Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 26 Feb 2026 13:02:42 -0500 Subject: [PATCH] feat: extract and save memories after chat conversations Co-Authored-By: Claude Opus 4.6 --- cogs/chat.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/cogs/chat.py b/cogs/chat.py index 0e5ceae..b86ef83 100644 --- a/cogs/chat.py +++ b/cogs/chat.py @@ -3,6 +3,7 @@ import logging import random import re from collections import deque +from datetime import datetime, timedelta, timezone from pathlib import Path import discord @@ -32,6 +33,8 @@ class ChatCog(commands.Cog): self._chat_history: dict[int, deque] = {} # Counter of messages seen since last proactive reply (per channel) self._messages_since_reply: dict[int, int] = {} + # Users whose profile has been updated and needs DB flush + self._dirty_users: set[int] = set() def _get_active_prompt(self) -> str: """Load the chat prompt for the current mode.""" @@ -39,6 +42,51 @@ class ChatCog(commands.Cog): prompt_file = mode_config.get("prompt_file", "chat_personality.txt") return _load_prompt(prompt_file) + async def _extract_and_save_memories( + self, user_id: int, username: str, conversation: list[dict[str, str]], + ) -> None: + """Background task: extract memories from conversation and save them.""" + try: + current_profile = self.bot.drama_tracker.get_user_notes(user_id) + result = await self.bot.llm.extract_memories( + conversation, username, current_profile, + ) + if not result: + return + + # Save expiring memories + for mem in result.get("memories", []): + if mem["expiration"] == "permanent": + continue # permanent facts go into profile_update + exp_days = {"1d": 1, "3d": 3, "7d": 7, "30d": 30} + days = exp_days.get(mem["expiration"], 7) + expires_at = datetime.now(timezone.utc) + timedelta(days=days) + await self.bot.db.save_memory( + user_id=user_id, + memory=mem["memory"], + topics=",".join(mem["topics"]), + importance=mem["importance"], + expires_at=expires_at, + source="chat", + ) + # Prune if over cap + await self.bot.db.prune_excess_memories(user_id) + + # Update profile if warranted + profile_update = result.get("profile_update") + if profile_update: + self.bot.drama_tracker.set_user_profile(user_id, profile_update) + self._dirty_users.add(user_id) + + logger.info( + "Extracted %d memories for %s (profile_update=%s)", + len(result.get("memories", [])), + username, + bool(profile_update), + ) + except Exception: + logger.exception("Failed to extract memories for %s", username) + @commands.Cog.listener() async def on_message(self, message: discord.Message): if message.author.bot: @@ -265,6 +313,14 @@ class ChatCog(commands.Cog): await message.reply(response, mention_author=False) + # Fire-and-forget memory extraction + if not image_attachment: + asyncio.create_task(self._extract_and_save_memories( + message.author.id, + message.author.display_name, + list(self._chat_history[ch_id]), + )) + reply_type = "proactive" if is_proactive else "chat" logger.info( "%s reply in #%s to %s: %s",