2025-06-30 22:29:23 +08:00
|
|
|
|
import traceback
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
|
|
|
|
|
|
2025-09-23 10:48:44 +08:00
|
|
|
|
from src.storage.db.models import User
|
2025-09-01 22:37:03 +08:00
|
|
|
|
from server.utils.auth_middleware import get_admin_user
|
|
|
|
|
|
from src import graph_base, knowledge_base
|
2025-12-15 23:25:56 +08:00
|
|
|
|
from src.knowledge.adapters.factory import GraphAdapterFactory
|
|
|
|
|
|
from src.knowledge.adapters.base import GraphAdapter
|
2025-06-30 22:29:23 +08:00
|
|
|
|
from src.utils.logging_config import logger
|
|
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
graph = APIRouter(prefix="/graph", tags=["graph"])
|
2025-06-30 22:29:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
# =============================================================================
|
2025-12-15 23:25:56 +08:00
|
|
|
|
# === 统一图谱接口 (Unified Graph API) ===
|
2025-07-22 17:29:38 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-12-15 23:25:56 +08:00
|
|
|
|
async def _get_graph_adapter(db_id: str) -> GraphAdapter:
|
2025-06-30 22:29:23 +08:00
|
|
|
|
"""
|
2025-12-15 23:25:56 +08:00
|
|
|
|
根据数据库ID获取对应的图谱适配器
|
2025-07-18 11:29:12 +08:00
|
|
|
|
|
2025-06-30 22:29:23 +08:00
|
|
|
|
Args:
|
2025-12-15 23:25:56 +08:00
|
|
|
|
db_id: 数据库ID
|
2025-07-18 11:29:12 +08:00
|
|
|
|
|
2025-06-30 22:29:23 +08:00
|
|
|
|
Returns:
|
2025-12-15 23:25:56 +08:00
|
|
|
|
GraphAdapter: 对应的图谱适配器实例
|
2025-06-30 22:29:23 +08:00
|
|
|
|
"""
|
2025-12-20 15:00:49 +08:00
|
|
|
|
# 检查图数据库服务状态 (仅对 Upload 类型需要)
|
2025-12-15 23:25:56 +08:00
|
|
|
|
if not graph_base.is_running():
|
2025-12-20 15:00:49 +08:00
|
|
|
|
# 先尝试检测图谱类型,如果是不需要 graph_base 的类型则允许
|
|
|
|
|
|
graph_type = GraphAdapterFactory.detect_graph_type(db_id, knowledge_base)
|
|
|
|
|
|
if graph_type == "upload":
|
|
|
|
|
|
raise HTTPException(status_code=503, detail="Graph database service is not running")
|
|
|
|
|
|
|
|
|
|
|
|
# 使用工厂方法自动创建适配器
|
|
|
|
|
|
return GraphAdapterFactory.create_adapter_by_db_id(
|
|
|
|
|
|
db_id=db_id, knowledge_base_manager=knowledge_base, graph_db_instance=graph_base
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-07-02 02:38:36 +08:00
|
|
|
|
|
2025-12-20 15:00:49 +08:00
|
|
|
|
def _get_capabilities_from_metadata(metadata) -> dict:
|
|
|
|
|
|
"""从 GraphMetadata 对象提取 capabilities 字典"""
|
|
|
|
|
|
return {
|
|
|
|
|
|
"supports_embedding": metadata.supports_embedding,
|
|
|
|
|
|
"supports_threshold": metadata.supports_threshold,
|
|
|
|
|
|
}
|
2025-06-30 22:29:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-12-15 23:25:56 +08:00
|
|
|
|
@graph.get("/list")
|
|
|
|
|
|
async def get_graphs(current_user: User = Depends(get_admin_user)):
|
2025-07-22 17:29:38 +08:00
|
|
|
|
"""
|
2025-12-15 23:25:56 +08:00
|
|
|
|
获取所有可用的知识图谱列表
|
2025-07-22 17:29:38 +08:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
2025-12-20 15:00:49 +08:00
|
|
|
|
包含所有图谱信息的列表 (包括 Neo4j 和 LightRAG),以及每个类型的 capability 信息
|
2025-07-22 17:29:38 +08:00
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2025-12-15 23:25:56 +08:00
|
|
|
|
graphs = []
|
|
|
|
|
|
|
2025-12-20 15:00:49 +08:00
|
|
|
|
# 1. 获取默认 Neo4j 图谱信息 (Upload 类型)
|
2025-12-15 23:25:56 +08:00
|
|
|
|
neo4j_info = graph_base.get_graph_info()
|
|
|
|
|
|
if neo4j_info:
|
2025-12-20 15:00:49 +08:00
|
|
|
|
# 直接使用 Upload 适配器的默认 metadata
|
|
|
|
|
|
from src.knowledge.adapters.upload import UploadGraphAdapter
|
|
|
|
|
|
|
|
|
|
|
|
capabilities = _get_capabilities_from_metadata(UploadGraphAdapter._get_metadata(None))
|
|
|
|
|
|
|
2025-12-16 10:28:44 +08:00
|
|
|
|
graphs.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "neo4j",
|
|
|
|
|
|
"name": "默认图谱",
|
2025-12-20 15:00:49 +08:00
|
|
|
|
"type": "upload",
|
2025-12-16 10:28:44 +08:00
|
|
|
|
"description": "Default graph database for uploaded documents",
|
|
|
|
|
|
"status": neo4j_info.get("status", "unknown"),
|
|
|
|
|
|
"created_at": neo4j_info.get("last_updated"),
|
|
|
|
|
|
"node_count": neo4j_info.get("entity_count", 0),
|
|
|
|
|
|
"edge_count": neo4j_info.get("relationship_count", 0),
|
2025-12-20 15:00:49 +08:00
|
|
|
|
"capabilities": capabilities,
|
2025-12-16 10:28:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
)
|
2025-12-15 23:25:56 +08:00
|
|
|
|
|
|
|
|
|
|
# 2. 获取 LightRAG 数据库信息
|
|
|
|
|
|
lightrag_dbs = knowledge_base.get_lightrag_databases()
|
2025-12-20 15:00:49 +08:00
|
|
|
|
# 直接使用 LightRAG 适配器的默认 metadata
|
|
|
|
|
|
from src.knowledge.adapters.lightrag import LightRAGGraphAdapter
|
|
|
|
|
|
|
|
|
|
|
|
capabilities = _get_capabilities_from_metadata(LightRAGGraphAdapter._get_metadata(None))
|
|
|
|
|
|
|
2025-12-15 23:25:56 +08:00
|
|
|
|
for db in lightrag_dbs:
|
2025-12-20 15:00:49 +08:00
|
|
|
|
db_id = db.get("db_id")
|
|
|
|
|
|
|
2025-12-16 10:28:44 +08:00
|
|
|
|
graphs.append(
|
|
|
|
|
|
{
|
2025-12-20 15:00:49 +08:00
|
|
|
|
"id": db_id,
|
2025-12-16 10:28:44 +08:00
|
|
|
|
"name": db.get("name"),
|
|
|
|
|
|
"type": "lightrag",
|
|
|
|
|
|
"description": db.get("description"),
|
|
|
|
|
|
"status": "active", # LightRAG DBs are usually active if listed
|
|
|
|
|
|
"created_at": db.get("created_at"),
|
|
|
|
|
|
"metadata": db,
|
2025-12-20 15:00:49 +08:00
|
|
|
|
"capabilities": capabilities,
|
2025-12-16 10:28:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
)
|
2025-12-15 23:25:56 +08:00
|
|
|
|
|
|
|
|
|
|
return {"success": True, "data": graphs}
|
2025-07-22 17:29:38 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-12-15 23:25:56 +08:00
|
|
|
|
logger.error(f"Failed to list graphs: {e}")
|
|
|
|
|
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to list graphs: {str(e)}")
|
2025-07-22 17:29:38 +08:00
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-12-15 23:25:56 +08:00
|
|
|
|
@graph.get("/subgraph")
|
|
|
|
|
|
async def get_subgraph(
|
|
|
|
|
|
db_id: str = Query(..., description="知识图谱ID"),
|
|
|
|
|
|
node_label: str = Query("*", description="节点标签或查询关键词"),
|
|
|
|
|
|
max_depth: int = Query(2, description="最大深度", ge=1, le=5),
|
|
|
|
|
|
max_nodes: int = Query(100, description="最大节点数", ge=1, le=1000),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
2025-06-30 22:29:23 +08:00
|
|
|
|
):
|
|
|
|
|
|
"""
|
2025-12-15 23:25:56 +08:00
|
|
|
|
统一的子图查询接口
|
2025-07-18 11:29:12 +08:00
|
|
|
|
|
2025-06-30 22:29:23 +08:00
|
|
|
|
Args:
|
2025-12-15 23:25:56 +08:00
|
|
|
|
db_id: 图谱ID (LightRAG DB ID 或 "neo4j")
|
|
|
|
|
|
node_label: 查询关键词或标签
|
|
|
|
|
|
max_depth: 扩展深度
|
|
|
|
|
|
max_nodes: 返回最大节点数
|
2025-06-30 22:29:23 +08:00
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2025-12-15 23:25:56 +08:00
|
|
|
|
logger.info(f"Querying subgraph - db_id: {db_id}, label: {node_label}")
|
|
|
|
|
|
|
|
|
|
|
|
adapter = await _get_graph_adapter(db_id)
|
|
|
|
|
|
|
2025-12-20 15:00:49 +08:00
|
|
|
|
# 统一查询参数 - 适配器会根据自己的配置处理这些参数
|
|
|
|
|
|
query_kwargs = {
|
|
|
|
|
|
"keyword": node_label,
|
|
|
|
|
|
"max_depth": max_depth,
|
|
|
|
|
|
"max_nodes": max_nodes,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result_data = await adapter.query_nodes(**query_kwargs)
|
2025-07-02 02:38:36 +08:00
|
|
|
|
|
2025-12-15 23:25:56 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"data": result_data,
|
|
|
|
|
|
}
|
2025-07-02 02:38:36 +08:00
|
|
|
|
|
2025-07-21 18:18:47 +08:00
|
|
|
|
except HTTPException:
|
|
|
|
|
|
raise
|
2025-06-30 22:29:23 +08:00
|
|
|
|
except Exception as e:
|
2025-12-15 23:25:56 +08:00
|
|
|
|
logger.error(f"Failed to get subgraph: {e}")
|
2025-06-30 22:29:23 +08:00
|
|
|
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
2025-12-15 23:25:56 +08:00
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to get subgraph: {str(e)}")
|
2025-06-30 22:29:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-12-15 23:25:56 +08:00
|
|
|
|
@graph.get("/labels")
|
|
|
|
|
|
async def get_graph_labels(
|
2025-12-16 10:28:44 +08:00
|
|
|
|
db_id: str = Query(..., description="知识图谱ID"), current_user: User = Depends(get_admin_user)
|
2025-06-30 22:29:23 +08:00
|
|
|
|
):
|
|
|
|
|
|
"""
|
2025-12-15 23:25:56 +08:00
|
|
|
|
获取图谱的所有标签
|
2025-06-30 22:29:23 +08:00
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2025-12-20 15:00:49 +08:00
|
|
|
|
# 使用统一的适配器获取标签
|
2025-12-15 23:25:56 +08:00
|
|
|
|
adapter = await _get_graph_adapter(db_id)
|
|
|
|
|
|
labels = await adapter.get_labels()
|
|
|
|
|
|
return {"success": True, "data": {"labels": labels}}
|
2025-07-02 02:38:36 +08:00
|
|
|
|
|
2025-06-30 22:29:23 +08:00
|
|
|
|
except Exception as e:
|
2025-12-15 23:25:56 +08:00
|
|
|
|
logger.error(f"Failed to get labels: {e}")
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to get labels: {str(e)}")
|
2025-06-30 22:29:23 +08:00
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-12-15 23:25:56 +08:00
|
|
|
|
@graph.get("/stats")
|
|
|
|
|
|
async def get_graph_stats(
|
2025-12-16 10:28:44 +08:00
|
|
|
|
db_id: str = Query(..., description="知识图谱ID"), current_user: User = Depends(get_admin_user)
|
2025-06-30 22:29:23 +08:00
|
|
|
|
):
|
|
|
|
|
|
"""
|
2025-12-15 23:25:56 +08:00
|
|
|
|
获取图谱统计信息
|
2025-06-30 22:29:23 +08:00
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2025-12-20 15:00:49 +08:00
|
|
|
|
# 使用适配器的统计信息 (适用于 kb_ 开头的数据库和 LightRAG 数据库)
|
|
|
|
|
|
if db_id.startswith("kb_") or knowledge_base.is_lightrag_database(db_id):
|
|
|
|
|
|
adapter = await _get_graph_adapter(db_id)
|
|
|
|
|
|
stats_data = await adapter.get_stats()
|
|
|
|
|
|
return {"success": True, "data": stats_data}
|
2025-12-15 23:25:56 +08:00
|
|
|
|
else:
|
2025-12-20 15:00:49 +08:00
|
|
|
|
# Neo4j stats (直接管理的图谱)
|
2025-12-15 23:25:56 +08:00
|
|
|
|
info = graph_base.get_graph_info(graph_name=db_id)
|
|
|
|
|
|
if not info:
|
2025-12-16 10:28:44 +08:00
|
|
|
|
raise HTTPException(status_code=404, detail="Graph info not found")
|
2025-12-15 23:25:56 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"total_nodes": info.get("entity_count", 0),
|
|
|
|
|
|
"total_edges": info.get("relationship_count", 0),
|
|
|
|
|
|
# Neo4j info currently returns 'labels' list, not counts per label.
|
|
|
|
|
|
# Improving this would require updating GraphDatabase.get_graph_info
|
2025-12-16 10:28:44 +08:00
|
|
|
|
"entity_types": [{"type": label, "count": "N/A"} for label in info.get("labels", [])],
|
|
|
|
|
|
},
|
2025-12-15 23:25:56 +08:00
|
|
|
|
}
|
2025-07-02 02:38:36 +08:00
|
|
|
|
|
2025-06-30 22:29:23 +08:00
|
|
|
|
except Exception as e:
|
2025-12-15 23:25:56 +08:00
|
|
|
|
logger.error(f"Failed to get stats: {e}")
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to get stats: {str(e)}")
|
2025-07-22 17:29:38 +08:00
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-12-15 23:25:56 +08:00
|
|
|
|
@graph.get("/neo4j/nodes")
|
|
|
|
|
|
async def get_neo4j_nodes(
|
|
|
|
|
|
kgdb_name: str = Query(..., description="知识图谱数据库名称"),
|
|
|
|
|
|
num: int = Query(100, description="节点数量", ge=1, le=1000),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
):
|
|
|
|
|
|
"""(Deprecated) Use /graph/subgraph instead"""
|
|
|
|
|
|
response = await get_subgraph(db_id=kgdb_name, node_label="*", max_nodes=num, current_user=current_user)
|
|
|
|
|
|
return {"success": True, "result": response["data"], "message": "success"}
|
2025-07-02 02:38:36 +08:00
|
|
|
|
|
2025-12-15 23:25:56 +08:00
|
|
|
|
|
|
|
|
|
|
@graph.get("/neo4j/node")
|
|
|
|
|
|
async def get_neo4j_node(
|
|
|
|
|
|
entity_name: str = Query(..., description="实体名称"), current_user: User = Depends(get_admin_user)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""(Deprecated) Use /graph/subgraph instead"""
|
|
|
|
|
|
# neo4j/node uses query_nodes(keyword=entity_name)
|
|
|
|
|
|
response = await get_subgraph(db_id="neo4j", node_label=entity_name, current_user=current_user)
|
|
|
|
|
|
return {"success": True, "result": response["data"], "message": "success"}
|
2025-07-22 17:29:38 +08:00
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@graph.get("/neo4j/info")
|
|
|
|
|
|
async def get_neo4j_info(current_user: User = Depends(get_admin_user)):
|
|
|
|
|
|
"""获取Neo4j图数据库信息"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
graph_info = graph_base.get_graph_info()
|
|
|
|
|
|
if graph_info is None:
|
|
|
|
|
|
raise HTTPException(status_code=400, detail="图数据库获取出错")
|
2025-09-01 22:37:03 +08:00
|
|
|
|
return {"success": True, "data": graph_info}
|
2025-07-22 17:29:38 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"获取图数据库信息失败: {e}")
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"获取图数据库信息失败: {str(e)}")
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@graph.post("/neo4j/index-entities")
|
2025-09-01 22:37:03 +08:00
|
|
|
|
async def index_neo4j_entities(data: dict = Body(default={}), current_user: User = Depends(get_admin_user)):
|
2025-07-22 17:29:38 +08:00
|
|
|
|
"""为Neo4j图谱节点添加嵌入向量索引"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if not graph_base.is_running():
|
|
|
|
|
|
raise HTTPException(status_code=400, detail="图数据库未启动")
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
kgdb_name = data.get("kgdb_name", "neo4j")
|
2025-07-22 17:29:38 +08:00
|
|
|
|
count = graph_base.add_embedding_to_nodes(kgdb_name=kgdb_name)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"status": "success",
|
|
|
|
|
|
"message": f"已成功为{count}个节点添加嵌入向量",
|
2025-09-01 22:37:03 +08:00
|
|
|
|
"indexed_count": count,
|
2025-07-22 17:29:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"索引节点失败: {e}")
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"索引节点失败: {str(e)}")
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@graph.post("/neo4j/add-entities")
|
|
|
|
|
|
async def add_neo4j_entities(
|
2025-09-01 22:37:03 +08:00
|
|
|
|
file_path: str = Body(...), kgdb_name: str | None = Body(None), current_user: User = Depends(get_admin_user)
|
2025-07-22 17:29:38 +08:00
|
|
|
|
):
|
|
|
|
|
|
"""通过JSONL文件添加图谱实体到Neo4j"""
|
|
|
|
|
|
try:
|
2025-12-20 15:00:49 +08:00
|
|
|
|
# 验证文件路径
|
|
|
|
|
|
if not file_path or not isinstance(file_path, str):
|
|
|
|
|
|
return {"success": False, "message": "文件路径不能为空", "status": "failed"}
|
|
|
|
|
|
|
|
|
|
|
|
file_path = file_path.strip()
|
|
|
|
|
|
if not file_path:
|
|
|
|
|
|
return {"success": False, "message": "文件路径不能为空", "status": "failed"}
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
if not file_path.endswith(".jsonl"):
|
|
|
|
|
|
return {"success": False, "message": "文件格式错误,请上传jsonl文件", "status": "failed"}
|
2025-07-22 17:29:38 +08:00
|
|
|
|
|
|
|
|
|
|
await graph_base.jsonl_file_add_entity(file_path, kgdb_name)
|
2025-09-01 22:37:03 +08:00
|
|
|
|
return {"success": True, "message": "实体添加成功", "status": "success"}
|
2025-07-22 17:29:38 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"添加实体失败: {e}, {traceback.format_exc()}")
|
2025-09-01 22:37:03 +08:00
|
|
|
|
return {"success": False, "message": f"添加实体失败: {e}", "status": "failed"}
|