From 9722601bae6af67550701c1b7fc67f8d552eb456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=90=E5=8A=9B=E9=BD=90?= <162269739+lanceyq@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:27:33 +0800 Subject: [PATCH] Feature/episodic memory (#70) * [feature]episodic memory * [feature]episodic memory * [changes]AI review and modify code * [feature]Explicit memory * [feature]Explicit memory --- .../controllers/user_memory_controllers.py | 95 +++++- api/app/core/memory/models/graph_models.py | 10 + api/app/core/memory/models/triplet_models.py | 10 + .../extraction_orchestrator.py | 15 +- .../prompt/prompts/extract_triplet.jinja2 | 89 +++++- api/app/repositories/neo4j/cypher_queries.py | 8 +- api/app/schemas/user_memory_schema.py | 13 + api/app/services/user_memory_service.py | 298 +++++++++++++++++- 8 files changed, 510 insertions(+), 28 deletions(-) diff --git a/api/app/controllers/user_memory_controllers.py b/api/app/controllers/user_memory_controllers.py index a5378e4d..5fd9b841 100644 --- a/api/app/controllers/user_memory_controllers.py +++ b/api/app/controllers/user_memory_controllers.py @@ -23,6 +23,8 @@ from app.schemas.memory_storage_schema import GenerateCacheRequest from app.schemas.user_memory_schema import ( EpisodicMemoryOverviewRequest, EpisodicMemoryDetailsRequest, + ExplicitMemoryOverviewRequest, + ExplicitMemoryDetailsRequest, ) from app.schemas.end_user_schema import ( @@ -450,8 +452,7 @@ async def get_episodic_memory_overview_api( 获取情景记忆总览 返回指定用户的所有情景记忆列表,包括标题和创建时间。 - 标题通过LLM自动生成。 - 支持通过时间范围、情景类型和标题关键词进行筛选。 + 支持通过时间范围、情景类型和标题关键词进行筛选。 """ workspace_id = current_user.current_workspace_id @@ -541,3 +542,93 @@ async def get_episodic_memory_details_api( except Exception as e: api_logger.error(f"情景记忆详情查询失败: end_user_id={request.end_user_id}, summary_id={request.summary_id}, error={str(e)}") return fail(BizCode.INTERNAL_ERROR, "情景记忆详情查询失败", str(e)) + + +@router.post("/classifications/explicit-memory", response_model=ApiResponse) +async def get_explicit_memory_overview_api( + request: ExplicitMemoryOverviewRequest, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +) -> dict: + """ + 获取显性记忆总览 + + 返回指定用户的所有显性记忆列表,包括标题、完整内容、创建时间和情绪信息。 + """ + workspace_id = current_user.current_workspace_id + + # 检查用户是否已选择工作空间 + if workspace_id is None: + api_logger.warning(f"用户 {current_user.username} 尝试查询显性记忆总览但未选择工作空间") + return fail(BizCode.INVALID_PARAMETER, "请先切换到一个工作空间", "current_workspace_id is None") + + api_logger.info( + f"显性记忆总览查询请求: end_user_id={request.end_user_id}, user={current_user.username}, " + f"workspace={workspace_id}" + ) + + try: + # 调用Service层方法 + result = await user_memory_service.get_explicit_memory_overview( + db, request.end_user_id + ) + + api_logger.info( + f"成功获取显性记忆总览: end_user_id={request.end_user_id}, " + f"total={result['total']}" + ) + return success(data=result, msg="查询成功") + + except Exception as e: + api_logger.error(f"显性记忆总览查询失败: end_user_id={request.end_user_id}, error={str(e)}") + return fail(BizCode.INTERNAL_ERROR, "显性记忆总览查询失败", str(e)) + + +@router.post("/classifications/explicit-memory-details", response_model=ApiResponse) +async def get_explicit_memory_details_api( + request: ExplicitMemoryDetailsRequest, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +) -> dict: + """ + 获取显性记忆详情 + + 根据 memory_id 返回情景记忆或语义记忆的详细信息。 + - 情景记忆:包括标题、内容、情绪、创建时间 + - 语义记忆:包括名称、核心定义、详细笔记、创建时间 + """ + workspace_id = current_user.current_workspace_id + + # 检查用户是否已选择工作空间 + if workspace_id is None: + api_logger.warning(f"用户 {current_user.username} 尝试查询显性记忆详情但未选择工作空间") + return fail(BizCode.INVALID_PARAMETER, "请先切换到一个工作空间", "current_workspace_id is None") + + api_logger.info( + f"显性记忆详情查询请求: end_user_id={request.end_user_id}, memory_id={request.memory_id}, " + f"user={current_user.username}, workspace={workspace_id}" + ) + + try: + # 调用Service层方法 + result = await user_memory_service.get_explicit_memory_details( + db=db, + end_user_id=request.end_user_id, + memory_id=request.memory_id + ) + + api_logger.info( + f"成功获取显性记忆详情: end_user_id={request.end_user_id}, memory_id={request.memory_id}, " + f"memory_type={result.get('memory_type')}" + ) + return success(data=result, msg="查询成功") + + except ValueError as e: + # 处理记忆不存在的情况 + api_logger.warning(f"显性记忆不存在: end_user_id={request.end_user_id}, memory_id={request.memory_id}, error={str(e)}") + return fail(BizCode.INVALID_PARAMETER, "显性记忆不存在", str(e)) + except Exception as e: + api_logger.error(f"显性记忆详情查询失败: end_user_id={request.end_user_id}, memory_id={request.memory_id}, error={str(e)}") + return fail(BizCode.INTERNAL_ERROR, "显性记忆详情查询失败", str(e)) + + diff --git a/api/app/core/memory/models/graph_models.py b/api/app/core/memory/models/graph_models.py index 1254388b..39d618fc 100644 --- a/api/app/core/memory/models/graph_models.py +++ b/api/app/core/memory/models/graph_models.py @@ -405,6 +405,10 @@ class ExtractedEntityNode(Node): statement_id: str = Field(..., description="Statement this entity was extracted from") entity_type: str = Field(..., description="Type of the entity") description: str = Field(..., description="Entity description") + example: str = Field( + default="", + description="A concise example (around 20 characters) to help understand the entity" + ) aliases: List[str] = Field( default_factory=list, description="Entity aliases - alternative names for this entity" @@ -441,6 +445,12 @@ class ExtractedEntityNode(Node): description="Total number of times this node has been accessed" ) + # Explicit Memory Classification + is_explicit_memory: bool = Field( + default=False, + description="Whether this entity represents explicit/semantic memory (knowledge, concepts, definitions, theories, principles)" + ) + @field_validator('aliases', mode='before') @classmethod def validate_aliases_field(cls, v): # 字段验证器 自动清理和验证 aliases 字段 diff --git a/api/app/core/memory/models/triplet_models.py b/api/app/core/memory/models/triplet_models.py index b0a062a3..df7ee14b 100644 --- a/api/app/core/memory/models/triplet_models.py +++ b/api/app/core/memory/models/triplet_models.py @@ -38,10 +38,20 @@ class Entity(BaseModel): name_embedding: Optional[List[float]] = Field(None, description="Embedding vector for the entity name") type: str = Field(..., description="Type/category of the entity") description: str = Field(..., description="Description of the entity") + example: str = Field( + default="", + description="A concise example (around 20 characters) to help understand the entity" + ) aliases: List[str] = Field( default_factory=list, description="Alternative names for this entity (abbreviations, full names, translations, etc.)" ) + + # Explicit Memory Classification + is_explicit_memory: bool = Field( + default=False, + description="Whether this entity represents explicit/semantic memory (knowledge, concepts, definitions, theories, principles)" + ) class Triplet(BaseModel): diff --git a/api/app/core/memory/storage_services/extraction_engine/extraction_orchestrator.py b/api/app/core/memory/storage_services/extraction_engine/extraction_orchestrator.py index 7c2ed5f4..75aaa7df 100644 --- a/api/app/core/memory/storage_services/extraction_engine/extraction_orchestrator.py +++ b/api/app/core/memory/storage_services/extraction_engine/extraction_orchestrator.py @@ -42,7 +42,6 @@ from app.core.memory.storage_services.extraction_engine.deduplication.two_stage_ ) from app.core.memory.storage_services.extraction_engine.knowledge_extraction.embedding_generation import ( embedding_generation, - embedding_generation_all, generate_entity_embeddings_from_triplets, ) @@ -179,7 +178,7 @@ class ExtractionOrchestrator: for dialog in dialog_data_list: for chunk in dialog.chunks: all_statements_list.extend(chunk.statements) - total_statements = len(all_statements_list) + len(all_statements_list) # 步骤 2: 并行执行三元组提取、时间信息提取、情绪提取和基础嵌入生成 logger.info("步骤 2/6: 并行执行三元组提取、时间信息提取、情绪提取和嵌入生成") @@ -201,9 +200,9 @@ class ExtractionOrchestrator: all_entities_list.extend(triplet_info.entities) all_triplets_list.extend(triplet_info.triplets) - total_entities = len(all_entities_list) - total_triplets = len(all_triplets_list) - total_temporal = sum(len(temporal_map) for temporal_map in temporal_maps) + len(all_entities_list) + len(all_triplets_list) + sum(len(temporal_map) for temporal_map in temporal_maps) # 步骤 3: 生成实体嵌入(依赖三元组提取结果) logger.info("步骤 3/6: 生成实体嵌入") @@ -385,7 +384,7 @@ class ExtractionOrchestrator: # 用于跟踪已完成的陈述句数量 completed_statements = 0 - total_statements = len(all_statements) + len(all_statements) # 全局并行处理所有陈述句 async def extract_for_statement(stmt_data, stmt_index): @@ -497,7 +496,7 @@ class ExtractionOrchestrator: # 用于跟踪已完成的时间提取数量 completed_temporal = 0 - total_temporal_statements = len(all_statements) + len(all_statements) # 全局并行处理所有陈述句 async def extract_for_statement(stmt_data, stmt_index): @@ -1082,10 +1081,12 @@ class ExtractionOrchestrator: statement_id=statement.id, # 添加必需的 statement_id 字段 entity_type=getattr(entity, 'type', 'unknown'), # 使用 type 而不是 entity_type description=getattr(entity, 'description', ''), # 添加必需的 description 字段 + example=getattr(entity, 'example', ''), # 新增:传递示例字段 fact_summary=getattr(entity, 'fact_summary', ''), # 添加必需的 fact_summary 字段 connect_strength=entity_connect_strength if entity_connect_strength is not None else 'Strong', # 添加必需的 connect_strength 字段 aliases=getattr(entity, 'aliases', []) or [], # 传递从三元组提取阶段获取的aliases name_embedding=getattr(entity, 'name_embedding', None), + is_explicit_memory=getattr(entity, 'is_explicit_memory', False), # 新增:传递语义记忆标记 group_id=dialog_data.group_id, user_id=dialog_data.user_id, apply_id=dialog_data.apply_id, diff --git a/api/app/core/memory/utils/prompt/prompts/extract_triplet.jinja2 b/api/app/core/memory/utils/prompt/prompts/extract_triplet.jinja2 index 337b5d4f..03691a04 100644 --- a/api/app/core/memory/utils/prompt/prompts/extract_triplet.jinja2 +++ b/api/app/core/memory/utils/prompt/prompts/extract_triplet.jinja2 @@ -12,7 +12,34 @@ Extract entities and knowledge triplets from the given statement. ===Guidelines=== **Entity Extraction:** -- Extract entities with their types, context-independent descriptions, and aliases +- Extract entities with their types, context-independent descriptions, **concise examples**, aliases, and semantic memory classification +- **Semantic Memory Classification (is_explicit_memory):** + * Set to `true` if the entity represents **explicit/semantic memory**: + - **Concepts:** "Machine Learning", "Photosynthesis", "Democracy", "人工智能", "光合作用", "民主" + - **Knowledge:** "Python Programming Language", "Theory of Relativity", "Python编程语言", "相对论" + - **Definitions:** "API (Application Programming Interface)", "REST API", "应用程序接口" + - **Principles:** "SOLID Principles", "First Law of Thermodynamics", "SOLID原则", "热力学第一定律" + - **Theories:** "Evolution Theory", "Quantum Mechanics", "进化论", "量子力学" + - **Methods/Techniques:** "Agile Development", "Machine Learning Algorithm", "敏捷开发", "机器学习算法" + - **Technical Terms:** "Neural Network", "Database", "神经网络", "数据库" + * Set to `false` for: + - **People:** "John Smith", "Dr. Wang", "张明", "王博士" + - **Organizations:** "Microsoft", "Harvard University", "微软", "哈佛大学" + - **Locations:** "Beijing", "Central Park", "北京", "中央公园" + - **Events:** "2024 Conference", "Project Meeting", "2024会议", "项目会议" + - **Specific objects:** "iPhone 15", "Building A", "iPhone 15", "A栋" +- **Example Generation (IMPORTANT for semantic memory entities):** + * For entities where `is_explicit_memory=true`, generate a **concise example (around 20 characters)** to help understand the concept + * The example should be: + - **Specific and concrete**: Use real-world scenarios or applications + - **Brief**: Around 20 characters (can be slightly longer if needed for clarity) + - **In the same language as the entity name** + * Examples: + - Entity: "机器学习" → example: "如:用神经网络识别图片中的猫狗" + - Entity: "SOLID Principles" → example: "e.g., Single Responsibility, Open-Closed" + - Entity: "Photosynthesis" → example: "e.g., plants convert sunlight to energy" + - Entity: "人工智能" → example: "如:智能客服、自动驾驶" + * For non-semantic entities (`is_explicit_memory=false`), the example field can be empty - **Aliases Extraction (Important):** * **CRITICAL: Extract aliases ONLY in the SAME LANGUAGE as the input text** * **DO NOT translate or add aliases in different languages** @@ -84,21 +111,27 @@ Output: "name": "I", "type": "Person", "description": "The user", - "aliases": [] + "example": "", + "aliases": [], + "is_explicit_memory": false }, { "entity_idx": 1, "name": "Paris", "type": "Location", "description": "Capital city of France", - "aliases": [] + "example": "", + "aliases": [], + "is_explicit_memory": false }, { "entity_idx": 2, "name": "Louvre", "type": "Location", "description": "World-famous museum located in Paris", - "aliases": ["Louvre Museum"] + "example": "", + "aliases": ["Louvre Museum"], + "is_explicit_memory": false } ] } @@ -130,21 +163,27 @@ Output: "name": "John Smith", "type": "Person", "description": "Individual person name", - "aliases": [] + "example": "", + "aliases": [], + "is_explicit_memory": false }, { "entity_idx": 1, "name": "Google", "type": "Organization", "description": "American technology company", - "aliases": ["Google LLC", "Alphabet Inc."] + "example": "", + "aliases": ["Google LLC", "Alphabet Inc."], + "is_explicit_memory": false }, { "entity_idx": 2, "name": "AI product development", - "type": "WorkRole", + "type": "Concept", "description": "Artificial intelligence product development work", - "aliases": [] + "example": "e.g., developing chatbots, recommendation systems", + "aliases": [], + "is_explicit_memory": true } ] } @@ -176,21 +215,27 @@ Output: "name": "我", "type": "Person", "description": "用户本人", - "aliases": [] + "example": "", + "aliases": [], + "is_explicit_memory": false }, { "entity_idx": 1, "name": "巴黎", "type": "Location", "description": "法国首都城市", - "aliases": [] + "example": "", + "aliases": [], + "is_explicit_memory": false }, { "entity_idx": 2, "name": "卢浮宫", "type": "Location", "description": "位于巴黎的世界著名博物馆", - "aliases": [] + "example": "", + "aliases": [], + "is_explicit_memory": false } ] } @@ -222,21 +267,27 @@ Output: "name": "张明", "type": "Person", "description": "个人姓名", - "aliases": [] + "example": "", + "aliases": [], + "is_explicit_memory": false }, { "entity_idx": 1, "name": "腾讯", "type": "Organization", "description": "中国科技公司", - "aliases": ["腾讯控股", "腾讯公司"] + "example": "", + "aliases": ["腾讯控股", "腾讯公司"], + "is_explicit_memory": false }, { "entity_idx": 2, "name": "AI产品开发", - "type": "WorkRole", + "type": "Concept", "description": "人工智能产品研发工作", - "aliases": [] + "example": "如:开发智能客服机器人、推荐系统", + "aliases": [], + "is_explicit_memory": true } ] } @@ -251,7 +302,9 @@ Output: "name": "Tripod", "type": "Equipment", "description": "Photography equipment accessory", - "aliases": ["Camera Tripod"] + "example": "", + "aliases": ["Camera Tripod"], + "is_explicit_memory": false } ] } @@ -266,7 +319,9 @@ Output: "name": "三脚架", "type": "Equipment", "description": "摄影器材配件", - "aliases": ["相机三脚架"] + "example": "", + "aliases": ["相机三脚架"], + "is_explicit_memory": false } ] } diff --git a/api/app/repositories/neo4j/cypher_queries.py b/api/app/repositories/neo4j/cypher_queries.py index 8c86cc4d..71a18c84 100644 --- a/api/app/repositories/neo4j/cypher_queries.py +++ b/api/app/repositories/neo4j/cypher_queries.py @@ -92,6 +92,11 @@ SET e.name = CASE WHEN entity.name IS NOT NULL AND entity.name <> '' THEN entity WHEN entity.description IS NOT NULL AND entity.description <> '' AND (e.description IS NULL OR size(e.description) = 0 OR size(entity.description) > size(e.description)) THEN entity.description ELSE e.description END, + e.example = CASE + WHEN entity.example IS NOT NULL AND entity.example <> '' + THEN entity.example + ELSE coalesce(e.example, '') + END, e.statement_id = CASE WHEN entity.statement_id IS NOT NULL AND entity.statement_id <> '' THEN entity.statement_id ELSE e.statement_id END, e.aliases = CASE WHEN entity.aliases IS NOT NULL AND size(entity.aliases) > 0 @@ -121,7 +126,8 @@ SET e.name = CASE WHEN entity.name IS NOT NULL AND entity.name <> '' THEN entity e.activation_value = CASE WHEN entity.activation_value IS NOT NULL THEN entity.activation_value ELSE e.activation_value END, e.access_history = CASE WHEN entity.access_history IS NOT NULL THEN entity.access_history ELSE coalesce(e.access_history, []) END, e.last_access_time = CASE WHEN entity.last_access_time IS NOT NULL THEN entity.last_access_time ELSE e.last_access_time END, - e.access_count = CASE WHEN entity.access_count IS NOT NULL THEN entity.access_count ELSE coalesce(e.access_count, 0) END + e.access_count = CASE WHEN entity.access_count IS NOT NULL THEN entity.access_count ELSE coalesce(e.access_count, 0) END, + e.is_explicit_memory = CASE WHEN entity.is_explicit_memory IS NOT NULL THEN entity.is_explicit_memory ELSE coalesce(e.is_explicit_memory, false) END RETURN e.id AS uuid """ diff --git a/api/app/schemas/user_memory_schema.py b/api/app/schemas/user_memory_schema.py index 27a458b6..796ad72f 100644 --- a/api/app/schemas/user_memory_schema.py +++ b/api/app/schemas/user_memory_schema.py @@ -28,3 +28,16 @@ class EpisodicMemoryDetailsRequest(BaseModel): end_user_id: str = Field(..., description="终端用户ID") summary_id: str = Field(..., description="情景记忆摘要ID") + + +class ExplicitMemoryOverviewRequest(BaseModel): + """显性记忆总览查询请求""" + + end_user_id: str = Field(..., description="终端用户ID") + + +class ExplicitMemoryDetailsRequest(BaseModel): + """显性记忆详情查询请求""" + + end_user_id: str = Field(..., description="终端用户ID") + memory_id: str = Field(..., description="记忆ID(情景记忆或语义记忆的ID)") diff --git a/api/app/services/user_memory_service.py b/api/app/services/user_memory_service.py index 3f0da196..b77a4ada 100644 --- a/api/app/services/user_memory_service.py +++ b/api/app/services/user_memory_service.py @@ -1441,12 +1441,308 @@ class UserMemoryService: return details - except ValueError as e: + except ValueError: # 重新抛出ValueError,让Controller层处理 raise except Exception as e: logger.error(f"获取情景记忆详情时出错: {str(e)}", exc_info=True) raise + + async def get_explicit_memory_overview( + self, + db: Session, + end_user_id: str + ) -> Dict[str, Any]: + """ + 获取显性记忆总览信息 + + 返回两部分: + 1. 情景记忆(episodic_memories)- 来自MemorySummary节点 + 2. 语义记忆(semantic_memories)- 来自ExtractedEntity节点(is_explicit_memory=true) + + Args: + db: 数据库会话 + end_user_id: 终端用户ID + + Returns: + { + "total": int, + "episodic_memories": [ + { + "id": str, + "title": str, + "content": str, + "created_at": int, + "emotion": Dict + } + ], + "semantic_memories": [ + { + "id": str, + "name": str, + "entity_type": str, + "core_definition": str, + "detailed_notes": str, + "created_at": int + } + ] + } + """ + try: + logger.info(f"开始查询 end_user_id={end_user_id} 的显性记忆总览(情景记忆+语义记忆)") + + # ========== 1. 查询情景记忆(MemorySummary节点) ========== + episodic_query = """ + MATCH (s:MemorySummary) + WHERE s.group_id = $group_id + RETURN elementId(s) AS id, + s.name AS title, + s.content AS content, + s.created_at AS created_at + ORDER BY s.created_at DESC + """ + + episodic_result = await self.neo4j_connector.execute_query( + episodic_query, + group_id=end_user_id + ) + + # 处理情景记忆数据 + episodic_memories = [] + if episodic_result: + for record in episodic_result: + summary_id = record["id"] + title = record.get("title") or "未命名" + content = record.get("content") or "" + created_at_str = record.get("created_at") + + # 转换时间戳 + created_at_timestamp = None + if created_at_str: + try: + from datetime import datetime + dt_object = datetime.fromisoformat(created_at_str.replace("Z", "+00:00")) + created_at_timestamp = int(dt_object.timestamp() * 1000) + except (ValueError, TypeError, AttributeError) as e: + logger.warning(f"无法解析时间戳: {created_at_str}, error={str(e)}") + + # 注意:总览接口不返回 emotion 字段 + episodic_memories.append({ + "id": summary_id, + "title": title, + "content": content, + "created_at": created_at_timestamp + }) + + # ========== 2. 查询语义记忆(ExtractedEntity节点) ========== + semantic_query = """ + MATCH (e:ExtractedEntity) + WHERE e.group_id = $group_id + AND e.is_explicit_memory = true + RETURN elementId(e) AS id, + e.name AS name, + e.entity_type AS entity_type, + e.description AS core_definition, + e.example AS detailed_notes, + e.created_at AS created_at + ORDER BY e.created_at DESC + """ + + semantic_result = await self.neo4j_connector.execute_query( + semantic_query, + group_id=end_user_id + ) + + # 处理语义记忆数据 + semantic_memories = [] + if semantic_result: + for record in semantic_result: + entity_id = record["id"] + name = record.get("name") or "未命名" + entity_type = record.get("entity_type") or "未分类" + core_definition = record.get("core_definition") or "" + created_at_str = record.get("created_at") + + # 转换时间戳 + created_at_timestamp = None + if created_at_str: + try: + from datetime import datetime + dt_object = datetime.fromisoformat(created_at_str.replace("Z", "+00:00")) + created_at_timestamp = int(dt_object.timestamp() * 1000) + except (ValueError, TypeError, AttributeError) as e: + logger.warning(f"无法解析时间戳: {created_at_str}, error={str(e)}") + + # 注意:总览接口不返回 detailed_notes 字段 + semantic_memories.append({ + "id": entity_id, + "name": name, + "entity_type": entity_type, + "core_definition": core_definition, + "created_at": created_at_timestamp + }) + + # ========== 3. 返回结果 ========== + total_count = len(episodic_memories) + len(semantic_memories) + + logger.info( + f"成功获取 end_user_id={end_user_id} 的显性记忆总览," + f"情景记忆={len(episodic_memories)} 条,语义记忆={len(semantic_memories)} 条," + f"总计 {total_count} 条" + ) + + return { + "total": total_count, + "episodic_memories": episodic_memories, + "semantic_memories": semantic_memories + } + + except Exception as e: + logger.error(f"获取显性记忆总览时出错: {str(e)}", exc_info=True) + raise + + async def get_explicit_memory_details( + self, + db: Session, + end_user_id: str, + memory_id: str + ) -> Dict[str, Any]: + """ + 获取显性记忆详情 + + 根据 memory_id 查询情景记忆或语义记忆的详细信息。 + 先尝试查询情景记忆,如果找不到再查询语义记忆。 + + Args: + db: 数据库会话 + end_user_id: 终端用户ID + memory_id: 记忆ID(可以是情景记忆或语义记忆的ID) + + Returns: + 情景记忆返回: + { + "memory_type": "episodic", + "title": str, + "content": str, + "emotion": Dict, + "created_at": int + } + + 语义记忆返回: + { + "memory_type": "semantic", + "name": str, + "core_definition": str, + "detailed_notes": str, + "created_at": int + } + + Raises: + ValueError: 当记忆不存在时 + """ + try: + logger.info(f"开始查询显性记忆详情: end_user_id={end_user_id}, memory_id={memory_id}") + + # ========== 1. 先尝试查询情景记忆 ========== + episodic_query = """ + MATCH (s:MemorySummary) + WHERE elementId(s) = $memory_id AND s.group_id = $group_id + RETURN s.name AS title, + s.content AS content, + s.created_at AS created_at + """ + + episodic_result = await self.neo4j_connector.execute_query( + episodic_query, + memory_id=memory_id, + group_id=end_user_id + ) + + if episodic_result and len(episodic_result) > 0: + record = episodic_result[0] + title = record.get("title") or "未命名" + content = record.get("content") or "" + created_at_str = record.get("created_at") + + # 转换时间戳 + created_at_timestamp = None + if created_at_str: + try: + from datetime import datetime + dt_object = datetime.fromisoformat(created_at_str.replace("Z", "+00:00")) + created_at_timestamp = int(dt_object.timestamp() * 1000) + except (ValueError, TypeError, AttributeError) as e: + logger.warning(f"无法解析时间戳: {created_at_str}, error={str(e)}") + + # 获取情绪信息 + emotion = await self._extract_episodic_emotion( + summary_id=memory_id, + end_user_id=end_user_id + ) + + logger.info(f"成功获取情景记忆详情: memory_id={memory_id}") + return { + "memory_type": "episodic", + "title": title, + "content": content, + "emotion": emotion, + "created_at": created_at_timestamp + } + + # ========== 2. 如果不是情景记忆,尝试查询语义记忆 ========== + semantic_query = """ + MATCH (e:ExtractedEntity) + WHERE elementId(e) = $memory_id + AND e.group_id = $group_id + AND e.is_explicit_memory = true + RETURN e.name AS name, + e.description AS core_definition, + e.example AS detailed_notes, + e.created_at AS created_at + """ + + semantic_result = await self.neo4j_connector.execute_query( + semantic_query, + memory_id=memory_id, + group_id=end_user_id + ) + + if semantic_result and len(semantic_result) > 0: + record = semantic_result[0] + name = record.get("name") or "未命名" + core_definition = record.get("core_definition") or "" + detailed_notes = record.get("detailed_notes") or "" + created_at_str = record.get("created_at") + + # 转换时间戳 + created_at_timestamp = None + if created_at_str: + try: + from datetime import datetime + dt_object = datetime.fromisoformat(created_at_str.replace("Z", "+00:00")) + created_at_timestamp = int(dt_object.timestamp() * 1000) + except (ValueError, TypeError, AttributeError) as e: + logger.warning(f"无法解析时间戳: {created_at_str}, error={str(e)}") + + logger.info(f"成功获取语义记忆详情: memory_id={memory_id}") + return { + "memory_type": "semantic", + "name": name, + "core_definition": core_definition, + "detailed_notes": detailed_notes, + "created_at": created_at_timestamp + } + + # ========== 3. 两种记忆都找不到 ========== + logger.warning(f"记忆不存在: memory_id={memory_id}, end_user_id={end_user_id}") + raise ValueError(f"记忆不存在: memory_id={memory_id}") + + except ValueError: + # 重新抛出 ValueError(记忆不存在) + raise + except Exception as e: + logger.error(f"获取显性记忆详情时出错: {str(e)}", exc_info=True) + raise # 独立的分析函数