"""会话目标自动持久化服务。 从用户消息自动持久化会话目标到 goals 表。 如果已有该 character 的活跃 session 级目标,判断内容是否显著不同: - 内容相似 → 复用 - 内容不同 → 完成旧目标,创建新目标 """ from app.models.governance import Goal from app.repositories import governance as governance_repo _TOPIC_SHIFT_MIN_LENGTH = 20 async def auto_persist_session_goal( session, character_id: str, content: str, ) -> Goal | None: """自动持久化会话目标:复用活跃的 session 目标或新建。 内容过短(<10 字)不产生目标。 如果已有活跃 session 目标但内容显著不同,先完成旧目标再新建。 Returns: 持久化后的 Goal(可能为 None)。 """ goal_text = (content or "").strip() if not goal_text or len(goal_text) < 10: return None existing = await governance_repo.list_active_session_goal(session, character_id) if existing is not None: # 判断是否话题转换:内容不再相似时完成旧目标 if _is_topic_shift(existing.title, goal_text): existing.status = "completed" existing.progress = 1.0 # 创建新目标 goal = Goal( character_id=character_id, title=goal_text[:120], description=f"从对话自动提取的会话目标:{goal_text[:500]}", level="session", type="mission", status="active", priority=50, ) await governance_repo.add_goal(session, goal) return goal return existing # 新建 session 目标 goal = Goal( character_id=character_id, title=goal_text[:120], description=f"从对话自动提取的会话目标:{goal_text[:500]}", level="session", type="mission", status="active", priority=50, ) await governance_repo.add_goal(session, goal) return goal def _is_topic_shift(existing_title: str, new_content: str) -> bool: """判断新内容与旧目标标题是否话题转换。 简单规则:如果新内容较长且与旧标题共享的关键词很少,视为话题转换。 这是启发式方法,后续可升级为 LLM 判断。 """ if len(new_content) < _TOPIC_SHIFT_MIN_LENGTH: return False # 提取关键词集合(按空格/标点分割) import re old_words = set(re.split(r"[\s,。!?、;:,\.!?;:]+", existing_title.lower())) new_words = set(re.split(r"[\s,。!?、;:,\.!?;:]+", new_content.lower())) # 排除常见的助词 stop_words = {"的", "了", "是", "在", "有", "和", "就", "不", "人", "都", "一", "一个", "上", "也", "很", "到", "说", "要", "去", "你", "我", "他", "这", "那", "吗", "啊", "呢", "吧", "地", "得"} old_keywords = old_words - stop_words new_keywords = new_words - stop_words if not old_keywords or not new_keywords: return False overlap = old_keywords & new_keywords # 如果旧关键词的一个都没有出现在新内容中,或者重合率极低 if not overlap: return True return len(overlap) / max(len(old_keywords), len(new_keywords)) < 0.15