""" ProactiveInformer Checks conversation context and proactively informs user of relevant memories. Only fires probabilistically based on trigger type. """ import random from typing import TYPE_CHECKING if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession # Trigger types and their firing probabilities TRIGGERS = { "high_importance_topic": {"probability": 0.8}, "repeat_question": {"probability": 1.0}, "forgotten_context": {"probability": 0.5}, "pending_goal": {"probability": 0.3}, } # Message style templates for proactive informing INFORM_STYLE = { "casual": "对了,你之前提到...", "gentle": "不知道你有没有注意到...", "helpful": "我记起你关心这个,要不看看...", } # Keywords that indicate different trigger types TRIGGER_KEYWORDS = { "high_importance_topic": ["关于", "提到", "说过", "记得"], "repeat_question": ["之前", "上次", "以前", "好像问过"], "forgotten_context": ["忘了", "不记得", "记不清", "之前聊过"], "pending_goal": ["目标", "计划", "想做", "打算", "想学"], } class ProactiveInformer: """Proactively inform users of relevant memories based on conversation context.""" def should_inform(self, trigger_type: str) -> bool: """Check if should inform based on probability.""" if trigger_type not in TRIGGERS: return False probability = TRIGGERS[trigger_type]["probability"] return random.random() < probability def detect_trigger(self, message: str) -> str | None: """Detect which trigger type (if any) this message corresponds to.""" msg_lower = message.lower() for trigger_type, keywords in TRIGGER_KEYWORDS.items(): for keyword in keywords: if keyword in msg_lower: return trigger_type return None def get_inform_message(self, trigger_type: str, context: dict) -> str: """Generate natural proactive message based on trigger type.""" style = INFORM_STYLE.get(context.get("style", "casual"), INFORM_STYLE["casual"]) if trigger_type == "high_importance_topic": memory_content = context.get("memory_content", "") return f"{style}「{memory_content[:30]}」这个话题你很关心,要深入聊聊吗?" elif trigger_type == "repeat_question": return "你之前问过类似的问题,我之前的回答可能还有参考价值。" elif trigger_type == "forgotten_context": memory_content = context.get("memory_content", "") return f"这个话题你一个月前聊过:「{memory_content[:30]}」。要恢复一下吗?" elif trigger_type == "pending_goal": goal_content = context.get("goal_content", "") return f"你之前说要「{goal_content[:30]}」,有进展了吗?" return "" async def check_and_inform( self, db: "AsyncSession", user_id: str, current_message: str, ) -> str | None: """Main entry point. Returns proactive message if should inform, else None. """ # 1. Detect trigger type trigger_type = self.detect_trigger(current_message) if not trigger_type: return None # 2. Check probability if not self.should_inform(trigger_type): return None # 3. Fetch relevant context context = await self._fetch_trigger_context(db, user_id, trigger_type) if not context: return None # 4. Generate message return self.get_inform_message(trigger_type, context) async def _fetch_trigger_context( self, db: "AsyncSession", user_id: str, trigger_type: str, ) -> dict | None: """Fetch relevant context for the trigger type.""" from sqlalchemy import select from app.models.memory import UserMemory # Get most recent high-importance memory result = await db.execute( select(UserMemory) .where( UserMemory.user_id == user_id, UserMemory.importance_level == "high", UserMemory.is_archived == False, ) .order_by(UserMemory.last_accessed_at.desc()) .limit(1) ) memory = result.scalar_one_or_none() if not memory: return None return { "memory_content": memory.content, "style": "casual", "memory_id": str(memory.id), }