[add] Create community nodes

This commit is contained in:
lanceyq
2026-03-10 17:06:43 +08:00
parent 85770dc037
commit db8257b67a
5 changed files with 569 additions and 1 deletions

View File

@@ -0,0 +1,129 @@
"""Community 节点仓库
管理 Neo4j 中 Community 节点及 BELONGS_TO_COMMUNITY 边的 CRUD 操作。
"""
import logging
from typing import Dict, List, Optional
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
from app.repositories.neo4j.cypher_queries import (
COMMUNITY_NODE_UPSERT,
ENTITY_JOIN_COMMUNITY,
ENTITY_LEAVE_ALL_COMMUNITIES,
GET_ENTITY_NEIGHBORS,
GET_ALL_ENTITIES_FOR_USER,
GET_COMMUNITY_MEMBERS,
CHECK_USER_HAS_COMMUNITIES,
UPDATE_COMMUNITY_MEMBER_COUNT,
)
logger = logging.getLogger(__name__)
class CommunityRepository:
def __init__(self, connector: Neo4jConnector):
self.connector = connector
async def upsert_community(
self, community_id: str, end_user_id: str, member_count: int = 0
) -> Optional[str]:
"""创建或更新 Community 节点,返回 community_id。"""
try:
result = await self.connector.execute_query(
COMMUNITY_NODE_UPSERT,
community_id=community_id,
end_user_id=end_user_id,
member_count=member_count,
)
return result[0]["community_id"] if result else None
except Exception as e:
logger.error(f"upsert_community failed: {e}")
return None
async def assign_entity_to_community(
self, entity_id: str, community_id: str, end_user_id: str
) -> bool:
"""将实体关联到社区(先解除旧关联,再建立新关联)。"""
try:
await self.connector.execute_query(
ENTITY_LEAVE_ALL_COMMUNITIES,
entity_id=entity_id,
end_user_id=end_user_id,
)
result = await self.connector.execute_query(
ENTITY_JOIN_COMMUNITY,
entity_id=entity_id,
community_id=community_id,
end_user_id=end_user_id,
)
return bool(result)
except Exception as e:
logger.error(f"assign_entity_to_community failed: {e}")
return False
async def get_entity_neighbors(
self, entity_id: str, end_user_id: str
) -> List[Dict]:
"""查询实体的直接邻居及其社区归属。"""
try:
return await self.connector.execute_query(
GET_ENTITY_NEIGHBORS,
entity_id=entity_id,
end_user_id=end_user_id,
)
except Exception as e:
logger.error(f"get_entity_neighbors failed: {e}")
return []
async def get_all_entities(self, end_user_id: str) -> List[Dict]:
"""拉取某用户下所有实体及其当前社区归属。"""
try:
return await self.connector.execute_query(
GET_ALL_ENTITIES_FOR_USER,
end_user_id=end_user_id,
)
except Exception as e:
logger.error(f"get_all_entities failed: {e}")
return []
async def get_community_members(
self, community_id: str, end_user_id: str
) -> List[Dict]:
"""查询社区成员列表。"""
try:
return await self.connector.execute_query(
GET_COMMUNITY_MEMBERS,
community_id=community_id,
end_user_id=end_user_id,
)
except Exception as e:
logger.error(f"get_community_members failed: {e}")
return []
async def has_communities(self, end_user_id: str) -> bool:
"""检查该用户是否已有 Community 节点(用于判断全量 vs 增量)。"""
try:
result = await self.connector.execute_query(
CHECK_USER_HAS_COMMUNITIES,
end_user_id=end_user_id,
)
return result[0]["community_count"] > 0 if result else False
except Exception as e:
logger.error(f"has_communities failed: {e}")
return False
async def refresh_member_count(
self, community_id: str, end_user_id: str
) -> int:
"""重新统计并更新社区成员数,返回最新数量。"""
try:
result = await self.connector.execute_query(
UPDATE_COMMUNITY_MEMBER_COUNT,
community_id=community_id,
end_user_id=end_user_id,
)
return result[0]["member_count"] if result else 0
except Exception as e:
logger.error(f"refresh_member_count failed: {e}")
return 0

View File

@@ -1058,4 +1058,95 @@ Graph_Node_query = """
3 AS priority
LIMIT $limit
"""
"""
# ============================================================
# Community 节点 & BELONGS_TO_COMMUNITY 边
# ============================================================
COMMUNITY_NODE_SAVE = """
MERGE (c:Community {community_id: $community_id})
SET c.end_user_id = $end_user_id,
c.formed_at = $formed_at,
c.updated_at = datetime(),
c.status = $status,
c.member_count = $member_count
RETURN c.community_id AS community_id
"""
COMMUNITY_ADD_MEMBER = """
MATCH (e:ExtractedEntity {id: $entity_id, end_user_id: $end_user_id})
MATCH (c:Community {community_id: $community_id, end_user_id: $end_user_id})
MERGE (e)-[:BELONGS_TO_COMMUNITY]->(c)
SET c.updated_at = datetime(),
c.member_count = $member_count
"""
# ─── Community 聚类相关 Cypher 模板 ───────────────────────────────────────────
COMMUNITY_NODE_UPSERT = """
MERGE (c:Community {community_id: $community_id})
SET c.end_user_id = $end_user_id,
c.member_count = $member_count,
c.updated_at = datetime()
RETURN c.community_id AS community_id
"""
ENTITY_JOIN_COMMUNITY = """
MATCH (e:ExtractedEntity {id: $entity_id, end_user_id: $end_user_id})
MATCH (c:Community {community_id: $community_id, end_user_id: $end_user_id})
MERGE (e)-[:BELONGS_TO_COMMUNITY]->(c)
SET c.updated_at = datetime()
RETURN e.id AS entity_id, c.community_id AS community_id
"""
ENTITY_LEAVE_ALL_COMMUNITIES = """
MATCH (e:ExtractedEntity {id: $entity_id, end_user_id: $end_user_id})
MATCH (e)-[r:BELONGS_TO_COMMUNITY]->(:Community)
DELETE r
"""
GET_ENTITY_NEIGHBORS = """
MATCH (e:ExtractedEntity {id: $entity_id, end_user_id: $end_user_id})
OPTIONAL MATCH (e)-[:EXTRACTED_RELATIONSHIP]-(nb:ExtractedEntity {end_user_id: $end_user_id})
OPTIONAL MATCH (nb)-[:BELONGS_TO_COMMUNITY]->(c:Community)
RETURN DISTINCT
nb.id AS id,
nb.name AS name,
nb.name_embedding AS name_embedding,
nb.activation_value AS activation_value,
CASE WHEN c IS NOT NULL THEN c.community_id ELSE null END AS community_id
"""
GET_ALL_ENTITIES_FOR_USER = """
MATCH (e:ExtractedEntity {end_user_id: $end_user_id})
OPTIONAL MATCH (e)-[:BELONGS_TO_COMMUNITY]->(c:Community)
RETURN e.id AS id,
e.name AS name,
e.name_embedding AS name_embedding,
e.activation_value AS activation_value,
CASE WHEN c IS NOT NULL THEN c.community_id ELSE null END AS community_id
"""
GET_COMMUNITY_MEMBERS = """
MATCH (e:ExtractedEntity {end_user_id: $end_user_id})-[:BELONGS_TO_COMMUNITY]->(c:Community {community_id: $community_id})
RETURN e.id AS id, e.name AS name, e.entity_type AS entity_type,
e.importance_score AS importance_score, e.activation_value AS activation_value,
e.name_embedding AS name_embedding
ORDER BY coalesce(e.activation_value, 0) DESC
"""
CHECK_USER_HAS_COMMUNITIES = """
MATCH (c:Community {end_user_id: $end_user_id})
RETURN count(c) AS community_count
"""
UPDATE_COMMUNITY_MEMBER_COUNT = """
MATCH (e:ExtractedEntity {end_user_id: $end_user_id})-[:BELONGS_TO_COMMUNITY]->(c:Community {community_id: $community_id})
WITH c, count(e) AS cnt
SET c.member_count = cnt
RETURN c.community_id AS community_id, cnt AS member_count
"""

View File

@@ -1,3 +1,4 @@
import asyncio
from typing import List
# 使用新的仓储层
@@ -288,6 +289,14 @@ async def save_dialog_and_statements_to_neo4j(
}
logger.info("Transaction completed. Summary: %s", summary)
logger.debug("Full transaction results: %r", results)
# 写入成功后,触发聚类
if entity_nodes:
end_user_id = entity_nodes[0].end_user_id
new_entity_ids = [e.id for e in entity_nodes]
logger.info(f"[Clustering] 准备触发聚类,实体数: {len(new_entity_ids)}, end_user_id: {end_user_id}")
await _trigger_clustering(new_entity_ids, end_user_id)
return True
except Exception as e:
@@ -295,3 +304,28 @@ async def save_dialog_and_statements_to_neo4j(
print(f"Neo4j integration error: {e}")
print("Continuing without database storage...")
return False
async def _trigger_clustering(
new_entity_ids: List[str],
end_user_id: str,
) -> None:
"""
聚类触发函数,自动判断全量初始化还是增量更新。
"""
connector = None
try:
from app.core.memory.storage_services.clustering_engine import LabelPropagationEngine
logger.info(f"[Clustering] 开始聚类end_user_id={end_user_id}, 实体数={len(new_entity_ids)}")
connector = Neo4jConnector()
engine = LabelPropagationEngine(connector)
await engine.run(end_user_id=end_user_id, new_entity_ids=new_entity_ids)
logger.info(f"[Clustering] 聚类完成end_user_id={end_user_id}")
except Exception as e:
logger.error(f"[Clustering] 聚类触发失败: {e}", exc_info=True)
finally:
if connector:
try:
await connector.close()
except Exception:
pass