Phase 5: Add Neo4j graph database integration

- Add neo4j_manager.py with full graph operations support
- Data sync: projects, entities, relations to Neo4j
- Graph queries: shortest path, all paths, neighbors, common neighbors
- Graph algorithms: centrality analysis, community detection
- Add 11 new API endpoints for graph operations
- Update docker-compose.yml with Neo4j service
- Update requirements.txt with neo4j driver
This commit is contained in:
OpenClaw Bot
2026-02-20 12:04:41 +08:00
parent 44c07b9984
commit 403e1cde28
6 changed files with 1328 additions and 4 deletions

View File

@@ -74,6 +74,11 @@ try:
except ImportError:
EXPORT_AVAILABLE = False
try:
from neo4j_manager import get_neo4j_manager, sync_project_to_neo4j, NEO4J_AVAILABLE
except ImportError:
NEO4J_AVAILABLE = False
app = FastAPI(title="InsightFlow", version="0.3.0")
app.add_middleware(
@@ -2323,6 +2328,283 @@ async def export_transcript_markdown_endpoint(transcript_id: str):
)
# ==================== Neo4j Graph Database API ====================
class Neo4jSyncRequest(BaseModel):
project_id: str
class PathQueryRequest(BaseModel):
source_entity_id: str
target_entity_id: str
max_depth: int = 10
class GraphQueryRequest(BaseModel):
entity_ids: List[str]
depth: int = 1
@app.get("/api/v1/neo4j/status")
async def neo4j_status():
"""获取 Neo4j 连接状态"""
if not NEO4J_AVAILABLE:
return {
"available": False,
"connected": False,
"message": "Neo4j driver not installed"
}
try:
manager = get_neo4j_manager()
connected = manager.is_connected()
return {
"available": True,
"connected": connected,
"uri": manager.uri if connected else None,
"message": "Connected" if connected else "Not connected"
}
except Exception as e:
return {
"available": True,
"connected": False,
"message": str(e)
}
@app.post("/api/v1/neo4j/sync")
async def neo4j_sync_project(request: Neo4jSyncRequest):
"""同步项目数据到 Neo4j"""
if not NEO4J_AVAILABLE:
raise HTTPException(status_code=503, detail="Neo4j not available")
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
manager = get_neo4j_manager()
if not manager.is_connected():
raise HTTPException(status_code=503, detail="Neo4j not connected")
db = get_db_manager()
project = db.get_project(request.project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
# 获取项目所有实体
entities = db.get_project_entities(request.project_id)
entities_data = []
for e in entities:
entities_data.append({
"id": e.id,
"name": e.name,
"type": e.type,
"definition": e.definition,
"aliases": json.loads(e.aliases) if e.aliases else [],
"properties": e.attributes if hasattr(e, 'attributes') else {}
})
# 获取项目所有关系
relations = db.get_project_relations(request.project_id)
relations_data = []
for r in relations:
relations_data.append({
"id": r.id,
"source_entity_id": r.source_entity_id,
"target_entity_id": r.target_entity_id,
"relation_type": r.relation_type,
"evidence": r.evidence,
"properties": {}
})
# 同步到 Neo4j
sync_project_to_neo4j(
project_id=request.project_id,
project_name=project.name,
entities=entities_data,
relations=relations_data
)
return {
"success": True,
"project_id": request.project_id,
"entities_synced": len(entities_data),
"relations_synced": len(relations_data),
"message": f"Synced {len(entities_data)} entities and {len(relations_data)} relations to Neo4j"
}
@app.get("/api/v1/projects/{project_id}/graph/stats")
async def get_graph_stats(project_id: str):
"""获取项目图统计信息"""
if not NEO4J_AVAILABLE:
raise HTTPException(status_code=503, detail="Neo4j not available")
manager = get_neo4j_manager()
if not manager.is_connected():
raise HTTPException(status_code=503, detail="Neo4j not connected")
stats = manager.get_graph_stats(project_id)
return stats
@app.post("/api/v1/graph/shortest-path")
async def find_shortest_path(request: PathQueryRequest):
"""查找两个实体之间的最短路径"""
if not NEO4J_AVAILABLE:
raise HTTPException(status_code=503, detail="Neo4j not available")
manager = get_neo4j_manager()
if not manager.is_connected():
raise HTTPException(status_code=503, detail="Neo4j not connected")
path = manager.find_shortest_path(
request.source_entity_id,
request.target_entity_id,
request.max_depth
)
if not path:
return {
"found": False,
"message": "No path found between entities"
}
return {
"found": True,
"path": {
"nodes": path.nodes,
"relationships": path.relationships,
"length": path.length
}
}
@app.post("/api/v1/graph/paths")
async def find_all_paths(request: PathQueryRequest):
"""查找两个实体之间的所有路径"""
if not NEO4J_AVAILABLE:
raise HTTPException(status_code=503, detail="Neo4j not available")
manager = get_neo4j_manager()
if not manager.is_connected():
raise HTTPException(status_code=503, detail="Neo4j not connected")
paths = manager.find_all_paths(
request.source_entity_id,
request.target_entity_id,
request.max_depth
)
return {
"count": len(paths),
"paths": [
{
"nodes": p.nodes,
"relationships": p.relationships,
"length": p.length
}
for p in paths
]
}
@app.get("/api/v1/entities/{entity_id}/neighbors")
async def get_entity_neighbors(
entity_id: str,
relation_type: str = None,
limit: int = 50
):
"""获取实体的邻居节点"""
if not NEO4J_AVAILABLE:
raise HTTPException(status_code=503, detail="Neo4j not available")
manager = get_neo4j_manager()
if not manager.is_connected():
raise HTTPException(status_code=503, detail="Neo4j not connected")
neighbors = manager.find_neighbors(entity_id, relation_type, limit)
return {
"entity_id": entity_id,
"count": len(neighbors),
"neighbors": neighbors
}
@app.get("/api/v1/entities/{entity_id1}/common-neighbors/{entity_id2}")
async def get_common_neighbors(entity_id1: str, entity_id2: str):
"""获取两个实体的共同邻居"""
if not NEO4J_AVAILABLE:
raise HTTPException(status_code=503, detail="Neo4j not available")
manager = get_neo4j_manager()
if not manager.is_connected():
raise HTTPException(status_code=503, detail="Neo4j not connected")
common = manager.find_common_neighbors(entity_id1, entity_id2)
return {
"entity_id1": entity_id1,
"entity_id2": entity_id2,
"count": len(common),
"common_neighbors": common
}
@app.get("/api/v1/projects/{project_id}/graph/centrality")
async def get_centrality_analysis(
project_id: str,
metric: str = "degree"
):
"""获取中心性分析结果"""
if not NEO4J_AVAILABLE:
raise HTTPException(status_code=503, detail="Neo4j not available")
manager = get_neo4j_manager()
if not manager.is_connected():
raise HTTPException(status_code=503, detail="Neo4j not connected")
rankings = manager.find_central_entities(project_id, metric)
return {
"metric": metric,
"count": len(rankings),
"rankings": [
{
"entity_id": r.entity_id,
"entity_name": r.entity_name,
"score": r.score,
"rank": r.rank
}
for r in rankings
]
}
@app.get("/api/v1/projects/{project_id}/graph/communities")
async def get_communities(project_id: str):
"""获取社区发现结果"""
if not NEO4J_AVAILABLE:
raise HTTPException(status_code=503, detail="Neo4j not available")
manager = get_neo4j_manager()
if not manager.is_connected():
raise HTTPException(status_code=503, detail="Neo4j not connected")
communities = manager.detect_communities(project_id)
return {
"count": len(communities),
"communities": [
{
"community_id": c.community_id,
"size": c.size,
"density": c.density,
"nodes": c.nodes
}
for c in communities
]
}
@app.post("/api/v1/graph/subgraph")
async def get_subgraph(request: GraphQueryRequest):
"""获取子图"""
if not NEO4J_AVAILABLE:
raise HTTPException(status_code=503, detail="Neo4j not available")
manager = get_neo4j_manager()
if not manager.is_connected():
raise HTTPException(status_code=503, detail="Neo4j not connected")
subgraph = manager.get_subgraph(request.entity_ids, request.depth)
return subgraph
# Serve frontend - MUST be last to not override API routes
app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend")