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:
282
backend/main.py
282
backend/main.py
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user