Feature/episodic memory (#70)
* [feature]episodic memory * [feature]episodic memory * [changes]AI review and modify code * [feature]Explicit memory * [feature]Explicit memory
This commit is contained in:
@@ -23,6 +23,8 @@ from app.schemas.memory_storage_schema import GenerateCacheRequest
|
|||||||
from app.schemas.user_memory_schema import (
|
from app.schemas.user_memory_schema import (
|
||||||
EpisodicMemoryOverviewRequest,
|
EpisodicMemoryOverviewRequest,
|
||||||
EpisodicMemoryDetailsRequest,
|
EpisodicMemoryDetailsRequest,
|
||||||
|
ExplicitMemoryOverviewRequest,
|
||||||
|
ExplicitMemoryDetailsRequest,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.schemas.end_user_schema import (
|
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
|
workspace_id = current_user.current_workspace_id
|
||||||
@@ -541,3 +542,93 @@ async def get_episodic_memory_details_api(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
api_logger.error(f"情景记忆详情查询失败: end_user_id={request.end_user_id}, summary_id={request.summary_id}, error={str(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))
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -405,6 +405,10 @@ class ExtractedEntityNode(Node):
|
|||||||
statement_id: str = Field(..., description="Statement this entity was extracted from")
|
statement_id: str = Field(..., description="Statement this entity was extracted from")
|
||||||
entity_type: str = Field(..., description="Type of the entity")
|
entity_type: str = Field(..., description="Type of the entity")
|
||||||
description: str = Field(..., description="Entity description")
|
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(
|
aliases: List[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Entity aliases - alternative names for this entity"
|
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"
|
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')
|
@field_validator('aliases', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_aliases_field(cls, v): # 字段验证器 自动清理和验证 aliases 字段
|
def validate_aliases_field(cls, v): # 字段验证器 自动清理和验证 aliases 字段
|
||||||
|
|||||||
@@ -38,10 +38,20 @@ class Entity(BaseModel):
|
|||||||
name_embedding: Optional[List[float]] = Field(None, description="Embedding vector for the entity name")
|
name_embedding: Optional[List[float]] = Field(None, description="Embedding vector for the entity name")
|
||||||
type: str = Field(..., description="Type/category of the entity")
|
type: str = Field(..., description="Type/category of the entity")
|
||||||
description: str = Field(..., description="Description 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(
|
aliases: List[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Alternative names for this entity (abbreviations, full names, translations, etc.)"
|
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):
|
class Triplet(BaseModel):
|
||||||
|
|||||||
@@ -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 (
|
from app.core.memory.storage_services.extraction_engine.knowledge_extraction.embedding_generation import (
|
||||||
embedding_generation,
|
embedding_generation,
|
||||||
embedding_generation_all,
|
|
||||||
generate_entity_embeddings_from_triplets,
|
generate_entity_embeddings_from_triplets,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -179,7 +178,7 @@ class ExtractionOrchestrator:
|
|||||||
for dialog in dialog_data_list:
|
for dialog in dialog_data_list:
|
||||||
for chunk in dialog.chunks:
|
for chunk in dialog.chunks:
|
||||||
all_statements_list.extend(chunk.statements)
|
all_statements_list.extend(chunk.statements)
|
||||||
total_statements = len(all_statements_list)
|
len(all_statements_list)
|
||||||
|
|
||||||
# 步骤 2: 并行执行三元组提取、时间信息提取、情绪提取和基础嵌入生成
|
# 步骤 2: 并行执行三元组提取、时间信息提取、情绪提取和基础嵌入生成
|
||||||
logger.info("步骤 2/6: 并行执行三元组提取、时间信息提取、情绪提取和嵌入生成")
|
logger.info("步骤 2/6: 并行执行三元组提取、时间信息提取、情绪提取和嵌入生成")
|
||||||
@@ -201,9 +200,9 @@ class ExtractionOrchestrator:
|
|||||||
all_entities_list.extend(triplet_info.entities)
|
all_entities_list.extend(triplet_info.entities)
|
||||||
all_triplets_list.extend(triplet_info.triplets)
|
all_triplets_list.extend(triplet_info.triplets)
|
||||||
|
|
||||||
total_entities = len(all_entities_list)
|
len(all_entities_list)
|
||||||
total_triplets = len(all_triplets_list)
|
len(all_triplets_list)
|
||||||
total_temporal = sum(len(temporal_map) for temporal_map in temporal_maps)
|
sum(len(temporal_map) for temporal_map in temporal_maps)
|
||||||
|
|
||||||
# 步骤 3: 生成实体嵌入(依赖三元组提取结果)
|
# 步骤 3: 生成实体嵌入(依赖三元组提取结果)
|
||||||
logger.info("步骤 3/6: 生成实体嵌入")
|
logger.info("步骤 3/6: 生成实体嵌入")
|
||||||
@@ -385,7 +384,7 @@ class ExtractionOrchestrator:
|
|||||||
|
|
||||||
# 用于跟踪已完成的陈述句数量
|
# 用于跟踪已完成的陈述句数量
|
||||||
completed_statements = 0
|
completed_statements = 0
|
||||||
total_statements = len(all_statements)
|
len(all_statements)
|
||||||
|
|
||||||
# 全局并行处理所有陈述句
|
# 全局并行处理所有陈述句
|
||||||
async def extract_for_statement(stmt_data, stmt_index):
|
async def extract_for_statement(stmt_data, stmt_index):
|
||||||
@@ -497,7 +496,7 @@ class ExtractionOrchestrator:
|
|||||||
|
|
||||||
# 用于跟踪已完成的时间提取数量
|
# 用于跟踪已完成的时间提取数量
|
||||||
completed_temporal = 0
|
completed_temporal = 0
|
||||||
total_temporal_statements = len(all_statements)
|
len(all_statements)
|
||||||
|
|
||||||
# 全局并行处理所有陈述句
|
# 全局并行处理所有陈述句
|
||||||
async def extract_for_statement(stmt_data, stmt_index):
|
async def extract_for_statement(stmt_data, stmt_index):
|
||||||
@@ -1082,10 +1081,12 @@ class ExtractionOrchestrator:
|
|||||||
statement_id=statement.id, # 添加必需的 statement_id 字段
|
statement_id=statement.id, # 添加必需的 statement_id 字段
|
||||||
entity_type=getattr(entity, 'type', 'unknown'), # 使用 type 而不是 entity_type
|
entity_type=getattr(entity, 'type', 'unknown'), # 使用 type 而不是 entity_type
|
||||||
description=getattr(entity, 'description', ''), # 添加必需的 description 字段
|
description=getattr(entity, 'description', ''), # 添加必需的 description 字段
|
||||||
|
example=getattr(entity, 'example', ''), # 新增:传递示例字段
|
||||||
fact_summary=getattr(entity, 'fact_summary', ''), # 添加必需的 fact_summary 字段
|
fact_summary=getattr(entity, 'fact_summary', ''), # 添加必需的 fact_summary 字段
|
||||||
connect_strength=entity_connect_strength if entity_connect_strength is not None else 'Strong', # 添加必需的 connect_strength 字段
|
connect_strength=entity_connect_strength if entity_connect_strength is not None else 'Strong', # 添加必需的 connect_strength 字段
|
||||||
aliases=getattr(entity, 'aliases', []) or [], # 传递从三元组提取阶段获取的aliases
|
aliases=getattr(entity, 'aliases', []) or [], # 传递从三元组提取阶段获取的aliases
|
||||||
name_embedding=getattr(entity, 'name_embedding', None),
|
name_embedding=getattr(entity, 'name_embedding', None),
|
||||||
|
is_explicit_memory=getattr(entity, 'is_explicit_memory', False), # 新增:传递语义记忆标记
|
||||||
group_id=dialog_data.group_id,
|
group_id=dialog_data.group_id,
|
||||||
user_id=dialog_data.user_id,
|
user_id=dialog_data.user_id,
|
||||||
apply_id=dialog_data.apply_id,
|
apply_id=dialog_data.apply_id,
|
||||||
|
|||||||
@@ -12,7 +12,34 @@ Extract entities and knowledge triplets from the given statement.
|
|||||||
===Guidelines===
|
===Guidelines===
|
||||||
|
|
||||||
**Entity Extraction:**
|
**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):**
|
- **Aliases Extraction (Important):**
|
||||||
* **CRITICAL: Extract aliases ONLY in the SAME LANGUAGE as the input text**
|
* **CRITICAL: Extract aliases ONLY in the SAME LANGUAGE as the input text**
|
||||||
* **DO NOT translate or add aliases in different languages**
|
* **DO NOT translate or add aliases in different languages**
|
||||||
@@ -84,21 +111,27 @@ Output:
|
|||||||
"name": "I",
|
"name": "I",
|
||||||
"type": "Person",
|
"type": "Person",
|
||||||
"description": "The user",
|
"description": "The user",
|
||||||
"aliases": []
|
"example": "",
|
||||||
|
"aliases": [],
|
||||||
|
"is_explicit_memory": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"entity_idx": 1,
|
"entity_idx": 1,
|
||||||
"name": "Paris",
|
"name": "Paris",
|
||||||
"type": "Location",
|
"type": "Location",
|
||||||
"description": "Capital city of France",
|
"description": "Capital city of France",
|
||||||
"aliases": []
|
"example": "",
|
||||||
|
"aliases": [],
|
||||||
|
"is_explicit_memory": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"entity_idx": 2,
|
"entity_idx": 2,
|
||||||
"name": "Louvre",
|
"name": "Louvre",
|
||||||
"type": "Location",
|
"type": "Location",
|
||||||
"description": "World-famous museum located in Paris",
|
"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",
|
"name": "John Smith",
|
||||||
"type": "Person",
|
"type": "Person",
|
||||||
"description": "Individual person name",
|
"description": "Individual person name",
|
||||||
"aliases": []
|
"example": "",
|
||||||
|
"aliases": [],
|
||||||
|
"is_explicit_memory": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"entity_idx": 1,
|
"entity_idx": 1,
|
||||||
"name": "Google",
|
"name": "Google",
|
||||||
"type": "Organization",
|
"type": "Organization",
|
||||||
"description": "American technology company",
|
"description": "American technology company",
|
||||||
"aliases": ["Google LLC", "Alphabet Inc."]
|
"example": "",
|
||||||
|
"aliases": ["Google LLC", "Alphabet Inc."],
|
||||||
|
"is_explicit_memory": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"entity_idx": 2,
|
"entity_idx": 2,
|
||||||
"name": "AI product development",
|
"name": "AI product development",
|
||||||
"type": "WorkRole",
|
"type": "Concept",
|
||||||
"description": "Artificial intelligence product development work",
|
"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": "我",
|
"name": "我",
|
||||||
"type": "Person",
|
"type": "Person",
|
||||||
"description": "用户本人",
|
"description": "用户本人",
|
||||||
"aliases": []
|
"example": "",
|
||||||
|
"aliases": [],
|
||||||
|
"is_explicit_memory": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"entity_idx": 1,
|
"entity_idx": 1,
|
||||||
"name": "巴黎",
|
"name": "巴黎",
|
||||||
"type": "Location",
|
"type": "Location",
|
||||||
"description": "法国首都城市",
|
"description": "法国首都城市",
|
||||||
"aliases": []
|
"example": "",
|
||||||
|
"aliases": [],
|
||||||
|
"is_explicit_memory": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"entity_idx": 2,
|
"entity_idx": 2,
|
||||||
"name": "卢浮宫",
|
"name": "卢浮宫",
|
||||||
"type": "Location",
|
"type": "Location",
|
||||||
"description": "位于巴黎的世界著名博物馆",
|
"description": "位于巴黎的世界著名博物馆",
|
||||||
"aliases": []
|
"example": "",
|
||||||
|
"aliases": [],
|
||||||
|
"is_explicit_memory": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -222,21 +267,27 @@ Output:
|
|||||||
"name": "张明",
|
"name": "张明",
|
||||||
"type": "Person",
|
"type": "Person",
|
||||||
"description": "个人姓名",
|
"description": "个人姓名",
|
||||||
"aliases": []
|
"example": "",
|
||||||
|
"aliases": [],
|
||||||
|
"is_explicit_memory": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"entity_idx": 1,
|
"entity_idx": 1,
|
||||||
"name": "腾讯",
|
"name": "腾讯",
|
||||||
"type": "Organization",
|
"type": "Organization",
|
||||||
"description": "中国科技公司",
|
"description": "中国科技公司",
|
||||||
"aliases": ["腾讯控股", "腾讯公司"]
|
"example": "",
|
||||||
|
"aliases": ["腾讯控股", "腾讯公司"],
|
||||||
|
"is_explicit_memory": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"entity_idx": 2,
|
"entity_idx": 2,
|
||||||
"name": "AI产品开发",
|
"name": "AI产品开发",
|
||||||
"type": "WorkRole",
|
"type": "Concept",
|
||||||
"description": "人工智能产品研发工作",
|
"description": "人工智能产品研发工作",
|
||||||
"aliases": []
|
"example": "如:开发智能客服机器人、推荐系统",
|
||||||
|
"aliases": [],
|
||||||
|
"is_explicit_memory": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -251,7 +302,9 @@ Output:
|
|||||||
"name": "Tripod",
|
"name": "Tripod",
|
||||||
"type": "Equipment",
|
"type": "Equipment",
|
||||||
"description": "Photography equipment accessory",
|
"description": "Photography equipment accessory",
|
||||||
"aliases": ["Camera Tripod"]
|
"example": "",
|
||||||
|
"aliases": ["Camera Tripod"],
|
||||||
|
"is_explicit_memory": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -266,7 +319,9 @@ Output:
|
|||||||
"name": "三脚架",
|
"name": "三脚架",
|
||||||
"type": "Equipment",
|
"type": "Equipment",
|
||||||
"description": "摄影器材配件",
|
"description": "摄影器材配件",
|
||||||
"aliases": ["相机三脚架"]
|
"example": "",
|
||||||
|
"aliases": ["相机三脚架"],
|
||||||
|
"is_explicit_memory": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 <> ''
|
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))
|
AND (e.description IS NULL OR size(e.description) = 0 OR size(entity.description) > size(e.description))
|
||||||
THEN entity.description ELSE e.description END,
|
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.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
|
e.aliases = CASE
|
||||||
WHEN entity.aliases IS NOT NULL AND size(entity.aliases) > 0
|
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.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.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.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
|
RETURN e.id AS uuid
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -28,3 +28,16 @@ class EpisodicMemoryDetailsRequest(BaseModel):
|
|||||||
|
|
||||||
end_user_id: str = Field(..., description="终端用户ID")
|
end_user_id: str = Field(..., description="终端用户ID")
|
||||||
summary_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)")
|
||||||
|
|||||||
@@ -1441,12 +1441,308 @@ class UserMemoryService:
|
|||||||
|
|
||||||
return details
|
return details
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError:
|
||||||
# 重新抛出ValueError,让Controller层处理
|
# 重新抛出ValueError,让Controller层处理
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取情景记忆详情时出错: {str(e)}", exc_info=True)
|
logger.error(f"获取情景记忆详情时出错: {str(e)}", exc_info=True)
|
||||||
raise
|
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
|
||||||
|
|
||||||
|
|
||||||
# 独立的分析函数
|
# 独立的分析函数
|
||||||
|
|||||||
Reference in New Issue
Block a user