diff --git a/bot.py b/bot.py index d56794b..6dd402d 100644 --- a/bot.py +++ b/bot.py @@ -61,7 +61,6 @@ WILDFILES_BASE_URL = os.environ.get("WILDFILES_BASE_URL", "") WILDFILES_ORG = os.environ.get("WILDFILES_ORG", "") USER_KEYS_FILE = os.environ.get("USER_KEYS_FILE", "/data/user_keys.json") MEMORIES_DIR = os.environ.get("MEMORIES_DIR", "/data/memories") -LANGUAGE_PREFS_FILE = os.environ.get("LANGUAGE_PREFS_FILE", "/data/language_prefs.json") MAX_MEMORIES_PER_USER = 50 SYSTEM_PROMPT = """You are a helpful AI assistant in a Matrix chat room. @@ -106,6 +105,7 @@ HELP_TEXT = """**AI Bot Commands** - `!ai wildfiles connect` — Connect your WildFiles account (opens browser approval) - `!ai wildfiles disconnect` — Disconnect your WildFiles account - `!ai auto-rename on|off` — Auto-rename room based on conversation topic +- `!ai memory ` — Explicitly tell the bot to remember something - `!ai forget` — Delete all memories the bot has about you - `!ai memories` — Show what the bot remembers about you - **Translate**: Forward a message to this DM — bot detects language and offers translation @@ -221,7 +221,6 @@ class Bot: self._pending_connects: dict[str, str] = {} # matrix_user_id -> device_code self._pending_translate: dict[str, dict] = {} # sender -> {text, detected_lang, room_id} self._pending_reply: dict[str, dict] = {} # sender -> {target_lang} - self.language_prefs: dict[str, str] = self._load_language_prefs() # user_id -> language @staticmethod def _load_user_keys() -> dict[str, str]: @@ -241,23 +240,6 @@ class Bot: except Exception: logger.exception("Failed to save user keys") - @staticmethod - def _load_language_prefs() -> dict[str, str]: - if os.path.exists(LANGUAGE_PREFS_FILE): - try: - with open(LANGUAGE_PREFS_FILE) as f: - return json.load(f) - except Exception: - logger.warning("Failed to load language prefs, starting fresh") - return {} - - def _save_language_prefs(self): - try: - with open(LANGUAGE_PREFS_FILE, "w") as f: - json.dump(self.language_prefs, f) - except Exception: - logger.exception("Failed to save language prefs") - async def start(self): # Restore existing session or create new one if os.path.exists(CREDS_FILE): @@ -473,6 +455,8 @@ class Bot: existing_facts = [m["fact"] for m in existing] existing_text = "\n".join(f"- {f}" for f in existing_facts) if existing_facts else "(none)" + logger.info("Memory extraction: user_msg=%s... (%d existing facts)", user_message[:80], len(existing_facts)) + try: resp = await self.llm.chat.completions.create( model=model, @@ -496,29 +480,30 @@ class Bot: max_tokens=300, ) raw = resp.choices[0].message.content.strip() - # Parse JSON array from response + logger.info("Memory extraction raw response: %s", raw[:200]) + + # Robust JSON extraction: strip markdown fences, find array if raw.startswith("```"): - raw = raw.split("\n", 1)[-1].rsplit("```", 1)[0] + raw = re.sub(r"^```\w*\n?", "", raw) + raw = re.sub(r"\n?```$", "", raw) + match = re.search(r"\[.*\]", raw, re.DOTALL) + if match: + raw = match.group(0) new_facts = json.loads(raw) if not isinstance(new_facts, list): + logger.warning("Memory extraction returned non-list: %s", type(new_facts)) return existing + logger.info("Memory extraction found %d new facts", len(new_facts)) + now = time.time() for fact in new_facts: if isinstance(fact, str) and fact.strip(): existing.append({"fact": fact.strip(), "created": now, "source_room": room_id}) - # Auto-detect language preference from new facts - fl = fact.lower() - if "language" in fl or "speaks" in fl or "prefers" in fl: - for lang in ["English", "German", "French", "Spanish", "Italian", - "Portuguese", "Dutch", "Russian", "Chinese", "Japanese", - "Korean", "Arabic", "Turkish", "Greek", "Hebrew"]: - if lang.lower() in fl: - self.language_prefs[sender] = lang - self._save_language_prefs() - logger.info("Auto-detected language preference: %s for %s", lang, sender) - break + return existing + except json.JSONDecodeError: + logger.warning("Memory extraction JSON parse failed, raw: %s", raw[:200]) return existing except Exception: logger.warning("Memory extraction failed", exc_info=True) @@ -561,11 +546,7 @@ class Bot: return f"[Translation failed] {text}" def _get_preferred_language(self, user_id: str) -> str: - """Get user's preferred language from explicit prefs, then memories, default English.""" - # Check explicit language preference first - if user_id in self.language_prefs: - return self.language_prefs[user_id] - # Fallback: scan memories + """Get user's preferred language from memories (last match = most recent).""" memories = self._load_memories(user_id) known_langs = [ "English", "German", "French", "Spanish", "Italian", "Portuguese", @@ -574,13 +555,15 @@ class Bot: "Greek", "Hebrew", "Hindi", "Thai", "Vietnamese", "Indonesian", "Czech", "Romanian", "Hungarian", "Ukrainian", "Croatian", "Serbian", ] + result = "English" for m in memories: fact = m["fact"].lower() if "language" in fact or "speaks" in fact or "prefers" in fact: for lang in known_langs: if lang.lower() in fact: - return lang - return "English" + result = lang + break + return result async def on_text_message(self, room, event: RoomMessageText): """Handle text messages: commands and AI responses.""" @@ -967,6 +950,21 @@ class Bot: status = "enabled" if enabled else "disabled" await self._send_text(room.room_id, f"Auto-rename **{status}** for this room.") + elif cmd.startswith("memory "): + fact = cmd[7:].strip() + sender = event.sender if event else None + if not fact: + await self._send_text(room.room_id, "Usage: `!ai memory `") + return + if sender: + memories = self._load_memories(sender) + memories.append({"fact": fact, "created": time.time(), "source_room": room.room_id}) + self._save_memories(sender, memories) + await self._send_text(room.room_id, f"Remembered: {fact}") + logger.info("Explicit memory stored for %s: %s", sender, fact[:80]) + else: + await self._send_text(room.room_id, "Could not identify user.") + elif cmd == "forget": sender = event.sender if event else None if sender: @@ -975,6 +973,9 @@ class Bot: os.remove(path) except FileNotFoundError: pass + # Clear any in-memory caches for this user + self._pending_translate.pop(sender, None) + self._pending_reply.pop(sender, None) await self._send_text(room.room_id, "All my memories about you have been deleted.") else: await self._send_text(room.room_id, "Could not identify user.") @@ -1206,11 +1207,14 @@ class Bot: else: await self._send_text(room.room_id, reply) - # Extract and save new memories (fire-and-forget, don't block response) + # Extract and save new memories (after reply sent, with timeout) if sender and reply: try: - updated = await self._extract_memories( - user_message, reply, memories, model, sender, room.room_id + updated = await asyncio.wait_for( + self._extract_memories( + user_message, reply, memories, model, sender, room.room_id + ), + timeout=15.0, ) if len(updated) > len(memories): self._save_memories(sender, updated) @@ -1218,6 +1222,8 @@ class Bot: len(updated) - len(memories), sender, len(updated)) else: logger.info("No new memories extracted for %s", sender) + except asyncio.TimeoutError: + logger.warning("Memory extraction timed out for %s", sender) except Exception: logger.warning("Memory save failed", exc_info=True) diff --git a/docker-compose.yml b/docker-compose.yml index 4e649f0..d5b08f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,9 +18,7 @@ services: - WILDFILES_BASE_URL - WILDFILES_ORG volumes: - - bot-crypto:/data/crypto_store - - bot-memories:/data/memories + - bot-data:/data volumes: - bot-crypto: - bot-memories: + bot-data: