diff --git a/server/routers/graph_router.py b/server/routers/graph_router.py
index 3c024650..1f816cfa 100644
--- a/server/routers/graph_router.py
+++ b/server/routers/graph_router.py
@@ -5,13 +5,215 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Query
from src.storage.db.models import User
from server.utils.auth_middleware import get_admin_user
from src import graph_base, knowledge_base
+from src.knowledge.adapters.factory import GraphAdapterFactory
+from src.knowledge.adapters.base import GraphAdapter
from src.utils.logging_config import logger
graph = APIRouter(prefix="/graph", tags=["graph"])
# =============================================================================
-# === 子图查询分组 ===
+# === 统一图谱接口 (Unified Graph API) ===
+# =============================================================================
+
+
+async def _get_graph_adapter(db_id: str) -> GraphAdapter:
+ """
+ 根据数据库ID获取对应的图谱适配器
+
+ Args:
+ db_id: 数据库ID
+
+ Returns:
+ GraphAdapter: 对应的图谱适配器实例
+ """
+ # 1. 检查是否是 LightRAG 数据库
+ if knowledge_base.is_lightrag_database(db_id):
+ rag_instance = await knowledge_base._get_lightrag_instance(db_id)
+ if not rag_instance:
+ raise HTTPException(status_code=404, detail=f"LightRAG database {db_id} not found or inaccessible")
+ return GraphAdapterFactory.create_adapter("lightrag", lightrag_instance=rag_instance)
+
+ # 2. 默认为 Upload/Neo4j 数据库 (假设 db_id 为 "neo4j" 或其他 Neo4j 数据库名)
+ # 这里我们假设非 LightRAG 的 ID 都是 Neo4j 的数据库名
+ # 如果未来有更多类型,需要更完善的 ID 区分机制 (例如前缀)
+ if not graph_base.is_running():
+ raise HTTPException(status_code=503, detail="Graph database service is not running")
+
+ return GraphAdapterFactory.create_adapter("upload", graph_db_instance=graph_base, config={"kgdb_name": db_id})
+
+
+@graph.get("/list")
+async def get_graphs(current_user: User = Depends(get_admin_user)):
+ """
+ 获取所有可用的知识图谱列表
+
+ Returns:
+ 包含所有图谱信息的列表 (包括 Neo4j 和 LightRAG)
+ """
+ try:
+ graphs = []
+
+ # 1. 获取默认 Neo4j 图谱信息
+ neo4j_info = graph_base.get_graph_info()
+ if neo4j_info:
+ graphs.append({
+ "id": "neo4j",
+ "name": "默认图谱",
+ "type": "neo4j",
+ "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)
+ })
+
+ # 2. 获取 LightRAG 数据库信息
+ lightrag_dbs = knowledge_base.get_lightrag_databases()
+ for db in lightrag_dbs:
+ graphs.append({
+ "id": db.get("db_id"),
+ "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
+ })
+
+ return {"success": True, "data": graphs}
+
+ except Exception as e:
+ 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)}")
+
+
+@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),
+):
+ """
+ 统一的子图查询接口
+
+ Args:
+ db_id: 图谱ID (LightRAG DB ID 或 "neo4j")
+ node_label: 查询关键词或标签
+ max_depth: 扩展深度
+ max_nodes: 返回最大节点数
+ """
+ try:
+ logger.info(f"Querying subgraph - db_id: {db_id}, label: {node_label}")
+
+ adapter = await _get_graph_adapter(db_id)
+
+ # 统一查询参数
+ # 对于 UploadGraphAdapter, kgdb_name 通常通过 kwargs 传递
+ # 对于 LightRAGGraphAdapter, max_depth/max_nodes 通过 kwargs 传递
+ result_data = await adapter.query_nodes(
+ keyword=node_label,
+ max_depth=max_depth,
+ max_nodes=max_nodes,
+ kgdb_name=db_id if not knowledge_base.is_lightrag_database(db_id) else "neo4j"
+ )
+
+ return {
+ "success": True,
+ "data": result_data,
+ }
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Failed to get subgraph: {e}")
+ logger.error(f"Traceback: {traceback.format_exc()}")
+ raise HTTPException(status_code=500, detail=f"Failed to get subgraph: {str(e)}")
+
+
+@graph.get("/labels")
+async def get_graph_labels(
+ db_id: str = Query(..., description="知识图谱ID"),
+ current_user: User = Depends(get_admin_user)
+):
+ """
+ 获取图谱的所有标签
+ """
+ try:
+ adapter = await _get_graph_adapter(db_id)
+ labels = await adapter.get_labels()
+ return {"success": True, "data": {"labels": labels}}
+
+ except Exception as e:
+ logger.error(f"Failed to get labels: {e}")
+ raise HTTPException(status_code=500, detail=f"Failed to get labels: {str(e)}")
+
+
+@graph.get("/stats")
+async def get_graph_stats(
+ db_id: str = Query(..., description="知识图谱ID"),
+ current_user: User = Depends(get_admin_user)
+):
+ """
+ 获取图谱统计信息
+ """
+ try:
+ if knowledge_base.is_lightrag_database(db_id):
+ # 复用原有的 LightRAG 统计逻辑
+ # 这里暂时直接调用原有逻辑,理想情况下也应该封装进 Adapter
+ rag_instance = await knowledge_base._get_lightrag_instance(db_id)
+ if not rag_instance:
+ raise HTTPException(status_code=404, detail="Database not found")
+
+ knowledge_graph = await rag_instance.get_knowledge_graph(node_label="*", max_depth=1, max_nodes=10000)
+ entity_types = {}
+ for node in knowledge_graph.nodes:
+ entity_type = node.properties.get("entity_type", "unknown")
+ entity_types[entity_type] = entity_types.get(entity_type, 0) + 1
+
+ entity_types_list = [
+ {"type": k, "count": v}
+ for k, v in sorted(entity_types.items(), key=lambda x: x[1], reverse=True)
+ ]
+
+ return {
+ "success": True,
+ "data": {
+ "total_nodes": len(knowledge_graph.nodes),
+ "total_edges": len(knowledge_graph.edges),
+ "entity_types": entity_types_list
+ }
+ }
+ else:
+ # Neo4j stats
+ info = graph_base.get_graph_info(graph_name=db_id)
+ if not info:
+ raise HTTPException(status_code=404, detail="Graph info not found")
+
+ 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
+ "entity_types": [
+ {"type": label, "count": "N/A"}
+ for label in info.get("labels", [])
+ ]
+ }
+ }
+
+ except Exception as e:
+ logger.error(f"Failed to get stats: {e}")
+ raise HTTPException(status_code=500, detail=f"Failed to get stats: {str(e)}")
+
+
+# =============================================================================
+# === 兼容性接口 (Deprecated/Compatibility) ===
# =============================================================================
@@ -23,147 +225,40 @@ async def get_lightrag_subgraph(
max_nodes: int = Query(100, description="最大节点数", ge=1, le=1000),
current_user: User = Depends(get_admin_user),
):
- """
- 使用 LightRAG 原生方法获取知识图谱子图
-
- Args:
- db_id: LightRAG 数据库实例ID
- node_label: 节点标签,用于查找起始节点,使用 "*" 获取全图
- max_depth: 子图的最大深度
- max_nodes: 返回的最大节点数量
-
- Returns:
- 包含节点和边的知识图谱数据
- """
- try:
- logger.info(
- f"获取子图数据 - db_id: {db_id}, node_label: {node_label}, max_depth: {max_depth}, max_nodes: {max_nodes}"
- )
-
- # 检查是否是 LightRAG 数据库
- if not knowledge_base.is_lightrag_database(db_id):
- raise HTTPException(
- status_code=400, detail=f"数据库 {db_id} 不是 LightRAG 类型,图谱功能仅支持 LightRAG 知识库"
- )
-
- # 获取 LightRAG 实例
- rag_instance = await knowledge_base._get_lightrag_instance(db_id)
- if not rag_instance:
- raise HTTPException(status_code=404, detail=f"LightRAG 数据库 {db_id} 不存在或无法访问")
-
- # 使用 LightRAG 的原生 get_knowledge_graph 方法
- knowledge_graph = await rag_instance.get_knowledge_graph(
- node_label=node_label, max_depth=max_depth, max_nodes=max_nodes
- )
-
- # 将 LightRAG 的 KnowledgeGraph 格式转换为前端需要的格式
- nodes = []
- for node in knowledge_graph.nodes:
- nodes.append(
- {
- "id": node.id,
- "labels": node.labels,
- "entity_type": node.properties.get("entity_type", "unknown"),
- "properties": node.properties,
- }
- )
-
- edges = []
- for edge in knowledge_graph.edges:
- edges.append(
- {
- "id": edge.id,
- "source": edge.source,
- "target": edge.target,
- "type": edge.type,
- "properties": edge.properties,
- }
- )
-
- result = {
- "success": True,
- "data": {
- "nodes": nodes,
- "edges": edges,
- "is_truncated": knowledge_graph.is_truncated,
- "total_nodes": len(nodes),
- "total_edges": len(edges),
- },
- }
-
- logger.info(f"成功获取子图 - 节点数: {len(nodes)}, 边数: {len(edges)}")
- return result
-
- except HTTPException:
- # 重新抛出 HTTP 异常
- raise
- except Exception as e:
- logger.error(f"获取子图数据失败: {e}")
- logger.error(f"Traceback: {traceback.format_exc()}")
- raise HTTPException(status_code=500, detail=f"获取子图数据失败: {str(e)}")
+ """(Deprecated) Use /graph/subgraph instead"""
+ return await get_subgraph(
+ db_id=db_id,
+ node_label=node_label,
+ max_depth=max_depth,
+ max_nodes=max_nodes,
+ current_user=current_user
+ )
@graph.get("/lightrag/databases")
async def get_lightrag_databases(current_user: User = Depends(get_admin_user)):
- """
- 获取所有可用的 LightRAG 数据库
-
- Returns:
- 可用的 LightRAG 数据库列表
- """
+ """(Deprecated) Use /graph/list instead"""
try:
lightrag_databases = knowledge_base.get_lightrag_databases()
return {"success": True, "data": {"databases": lightrag_databases}}
-
except Exception as e:
- logger.error(f"获取 LightRAG 数据库列表失败: {e}")
- raise HTTPException(status_code=500, detail=f"获取 LightRAG 数据库列表失败: {str(e)}")
-
-
-# =============================================================================
-# === 节点管理分组 ===
-# =============================================================================
+ raise HTTPException(status_code=500, detail=str(e))
@graph.get("/lightrag/labels")
async def get_lightrag_labels(
db_id: str = Query(..., description="数据库ID"), current_user: User = Depends(get_admin_user)
):
- """
- 获取知识图谱中的所有标签
+ """(Deprecated) Use /graph/labels instead"""
+ return await get_graph_labels(db_id=db_id, current_user=current_user)
- Args:
- db_id: LightRAG 数据库实例ID
- Returns:
- 图谱中所有可用的标签列表
- """
- try:
- logger.info(f"获取图谱标签 - db_id: {db_id}")
-
- # 检查是否是 LightRAG 数据库
- if not knowledge_base.is_lightrag_database(db_id):
- raise HTTPException(
- status_code=400, detail=f"数据库 {db_id} 不是 LightRAG 类型,图谱功能仅支持 LightRAG 知识库"
- )
-
- # 获取 LightRAG 实例
- rag_instance = await knowledge_base._get_lightrag_instance(db_id)
- if not rag_instance:
- raise HTTPException(status_code=404, detail=f"LightRAG 数据库 {db_id} 不存在或无法访问")
-
- # 使用 LightRAG 的原生方法获取所有标签
- labels = await rag_instance.get_graph_labels()
-
- return {"success": True, "data": {"labels": labels}}
-
- except HTTPException:
- # 重新抛出 HTTP 异常
- raise
- except Exception as e:
- logger.error(f"获取图谱标签失败: {e}")
- logger.error(f"Traceback: {traceback.format_exc()}")
- raise HTTPException(status_code=500, detail=f"获取图谱标签失败: {str(e)}")
+@graph.get("/lightrag/stats")
+async def get_lightrag_stats(
+ db_id: str = Query(..., description="数据库ID"), current_user: User = Depends(get_admin_user)
+):
+ """(Deprecated) Use /graph/stats instead"""
+ return await get_graph_stats(db_id=db_id, current_user=current_user)
@graph.get("/neo4j/nodes")
@@ -172,110 +267,19 @@ async def get_neo4j_nodes(
num: int = Query(100, description="节点数量", ge=1, le=1000),
current_user: User = Depends(get_admin_user),
):
- """
- 获取图谱节点样本数据
- """
- try:
- logger.debug(f"Get graph nodes in {kgdb_name} with {num} nodes")
-
- if not graph_base.is_running():
- raise HTTPException(status_code=400, detail="图数据库未启动")
-
- result = graph_base.get_sample_nodes(kgdb_name, num)
-
- return {"success": True, "result": result, "message": "success"}
-
- except Exception as e:
- logger.error(f"获取图节点数据失败: {e}\n{traceback.format_exc()}")
- raise HTTPException(status_code=500, detail=f"获取图节点数据失败: {str(e)}")
+ """(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"}
@graph.get("/neo4j/node")
async def get_neo4j_node(
entity_name: str = Query(..., description="实体名称"), current_user: User = Depends(get_admin_user)
):
- """
- 根据实体名称查询图节点
- """
- try:
- if not graph_base.is_running():
- raise HTTPException(status_code=400, detail="图数据库未启动")
-
- result = graph_base.query_node(keyword=entity_name)
-
- return {"success": True, "result": result, "message": "success"}
-
- except Exception as e:
- logger.error(f"查询图节点失败: {e}\n{traceback.format_exc()}")
- raise HTTPException(status_code=500, detail=f"查询图节点失败: {str(e)}")
-
-
-# =============================================================================
-# === 边管理分组 ===
-# =============================================================================
-
-# 可以在这里添加边相关的管理功能
-
-# =============================================================================
-# === 图谱分析分组 ===
-# =============================================================================
-
-
-@graph.get("/lightrag/stats")
-async def get_lightrag_stats(
- db_id: str = Query(..., description="数据库ID"), current_user: User = Depends(get_admin_user)
-):
- """
- 获取知识图谱统计信息
- """
- try:
- logger.info(f"获取图谱统计信息 - db_id: {db_id}")
-
- # 检查是否是 LightRAG 数据库
- if not knowledge_base.is_lightrag_database(db_id):
- raise HTTPException(
- status_code=400, detail=f"数据库 {db_id} 不是 LightRAG 类型,图谱功能仅支持 LightRAG 知识库"
- )
-
- # 获取 LightRAG 实例
- rag_instance = await knowledge_base._get_lightrag_instance(db_id)
- if not rag_instance:
- raise HTTPException(status_code=404, detail=f"LightRAG 数据库 {db_id} 不存在或无法访问")
-
- # 通过获取全图来统计节点和边的数量
- knowledge_graph = await rag_instance.get_knowledge_graph(
- node_label="*",
- max_depth=1,
- max_nodes=10000, # 设置较大值以获取完整统计
- )
-
- # 统计实体类型分布
- entity_types = {}
- for node in knowledge_graph.nodes:
- entity_type = node.properties.get("entity_type", "unknown")
- entity_types[entity_type] = entity_types.get(entity_type, 0) + 1
-
- entity_types_list = [
- {"type": k, "count": v} for k, v in sorted(entity_types.items(), key=lambda x: x[1], reverse=True)
- ]
-
- return {
- "success": True,
- "data": {
- "total_nodes": len(knowledge_graph.nodes),
- "total_edges": len(knowledge_graph.edges),
- "entity_types": entity_types_list,
- "is_truncated": knowledge_graph.is_truncated,
- },
- }
-
- except HTTPException:
- # 重新抛出 HTTP 异常
- raise
- except Exception as e:
- logger.error(f"获取图谱统计信息失败: {e}")
- logger.error(f"Traceback: {traceback.format_exc()}")
- raise HTTPException(status_code=500, detail=f"获取图谱统计信息失败: {str(e)}")
+ """(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"}
@graph.get("/neo4j/info")
@@ -298,10 +302,7 @@ async def index_neo4j_entities(data: dict = Body(default={}), current_user: User
if not graph_base.is_running():
raise HTTPException(status_code=400, detail="图数据库未启动")
- # 获取参数或使用默认值
kgdb_name = data.get("kgdb_name", "neo4j")
-
- # 调用GraphDatabase的add_embedding_to_nodes方法
count = graph_base.add_embedding_to_nodes(kgdb_name=kgdb_name)
return {
diff --git a/src/agents/common/middlewares/dynamic_tool_middleware.py b/src/agents/common/middlewares/dynamic_tool_middleware.py
index acce5f4d..a8dc196b 100644
--- a/src/agents/common/middlewares/dynamic_tool_middleware.py
+++ b/src/agents/common/middlewares/dynamic_tool_middleware.py
@@ -60,7 +60,8 @@ class DynamicToolMiddleware(AgentMiddleware):
logger.warning(f"MCP server '{mcp}' not pre-loaded. Please add it to mcp_servers list.")
logger.info(
- f"Dynamic tool selection: {len(enabled_tools)} tools enabled: {[tool.name for tool in enabled_tools]}"
+ f"Dynamic tool selection: {len(enabled_tools)} tools enabled: {[tool.name for tool in enabled_tools]}, "
+ f"selected_tools: {selected_tools}, selected_mcps: {selected_mcps}"
)
# 更新 request 中的工具列表
diff --git a/src/knowledge/adapters/__init__.py b/src/knowledge/adapters/__init__.py
new file mode 100644
index 00000000..027747fc
--- /dev/null
+++ b/src/knowledge/adapters/__init__.py
@@ -0,0 +1,6 @@
+from .base import GraphAdapter
+from .factory import GraphAdapterFactory
+from .lightrag import LightRAGGraphAdapter
+from .upload import UploadGraphAdapter
+
+__all__ = ["GraphAdapter", "UploadGraphAdapter", "LightRAGGraphAdapter", "GraphAdapterFactory"]
diff --git a/src/knowledge/adapters/base.py b/src/knowledge/adapters/base.py
new file mode 100644
index 00000000..a757c278
--- /dev/null
+++ b/src/knowledge/adapters/base.py
@@ -0,0 +1,87 @@
+from abc import ABC, abstractmethod
+from typing import Any
+
+
+class GraphAdapter(ABC):
+ """图谱适配器基类 (Base Graph Adapter)"""
+
+ @abstractmethod
+ async def query_nodes(self, keyword: str, **kwargs) -> dict[str, Any]:
+ """查询节点 (Query nodes)"""
+ pass
+
+ @abstractmethod
+ async def add_entity(self, triples: list[dict], **kwargs) -> bool:
+ """添加实体三元组 (Add entity triples)"""
+ pass
+
+ @abstractmethod
+ async def get_sample_nodes(self, num: int = 50, **kwargs) -> dict[str, list]:
+ """获取样本节点 (Get sample nodes)"""
+ pass
+
+ @abstractmethod
+ def normalize_node(self, raw_node: Any) -> dict[str, Any]:
+ """标准化节点格式 (Normalize node format)"""
+ pass
+
+ @abstractmethod
+ def normalize_edge(self, raw_edge: Any) -> dict[str, Any]:
+ """标准化边格式 (Normalize edge format)"""
+ pass
+
+ @abstractmethod
+ async def get_labels(self) -> list[str]:
+ """获取所有标签 (Get all labels)"""
+ pass
+
+ def _create_standard_node(
+ self,
+ node_id: str,
+ name: str,
+ entity_type: str,
+ labels: list[str],
+ properties: dict[str, Any],
+ source: str,
+ ) -> dict[str, Any]:
+ """
+ Helper to create a standardized node dictionary.
+ """
+ return {
+ "id": node_id,
+ "name": name,
+ "original_id": node_id,
+ "type": entity_type,
+ "labels": labels,
+ "properties": properties,
+ "normalized": {
+ "name": name,
+ "type": entity_type,
+ "source": source,
+ },
+ "graph_type": source,
+ }
+
+ def _create_standard_edge(
+ self,
+ edge_id: str,
+ source_id: str,
+ target_id: str,
+ edge_type: str,
+ properties: dict[str, Any],
+ direction: str = "directed",
+ ) -> dict[str, Any]:
+ """
+ Helper to create a standardized edge dictionary.
+ """
+ return {
+ "id": edge_id,
+ "source_id": source_id,
+ "target_id": target_id,
+ "type": edge_type,
+ "properties": properties,
+ "normalized": {
+ "type": edge_type,
+ "direction": direction,
+ },
+ }
diff --git a/src/knowledge/adapters/factory.py b/src/knowledge/adapters/factory.py
new file mode 100644
index 00000000..66a23680
--- /dev/null
+++ b/src/knowledge/adapters/factory.py
@@ -0,0 +1,23 @@
+from .base import GraphAdapter
+from .lightrag import LightRAGGraphAdapter
+from .upload import UploadGraphAdapter
+
+
+class GraphAdapterFactory:
+ """图谱适配器工厂 (Graph Adapter Factory)"""
+
+ _registry: dict[str, type[GraphAdapter]] = {"upload": UploadGraphAdapter, "lightrag": LightRAGGraphAdapter}
+
+ @classmethod
+ def register(cls, graph_type: str, adapter_class: type[GraphAdapter]):
+ """注册适配器类 (Register adapter class)"""
+ cls._registry[graph_type] = adapter_class
+
+ @classmethod
+ def create_adapter(cls, graph_type: str, **kwargs) -> GraphAdapter:
+ """创建适配器实例 (Create adapter instance)"""
+ adapter_class = cls._registry.get(graph_type)
+ if not adapter_class:
+ raise ValueError(f"Unknown graph type: {graph_type}")
+
+ return adapter_class(**kwargs)
diff --git a/src/knowledge/adapters/lightrag.py b/src/knowledge/adapters/lightrag.py
new file mode 100644
index 00000000..f4b2206d
--- /dev/null
+++ b/src/knowledge/adapters/lightrag.py
@@ -0,0 +1,154 @@
+import logging
+from typing import Any
+
+from .base import GraphAdapter
+
+logger = logging.getLogger(__name__)
+
+
+class LightRAGGraphAdapter(GraphAdapter):
+ """LightRAG图谱适配器 (LightRAG Graph Adapter)"""
+
+ def __init__(self, lightrag_instance: Any, config: dict[str, Any] = None):
+ self.config = config or {
+ "node_label_field": "labels",
+ "id_field": "id",
+ "type_field": "entity_type",
+ "relation_prefix": "HAS_",
+ }
+ self.lightrag = lightrag_instance
+
+ async def query_nodes(self, keyword: str, **kwargs) -> dict[str, Any]:
+ # Map keyword to node_label
+ # If keyword is empty or "*", query all (or sample)
+ node_label = keyword if keyword and keyword != "*" else "*"
+
+ max_depth = kwargs.get("max_depth", 2)
+ max_nodes = kwargs.get("max_nodes", 100)
+
+ # lightrag.get_knowledge_graph is async
+ # Note: if node_label is "*", LightRAG might return a large graph or sample depending on implementation
+ raw_graph = await self.lightrag.get_knowledge_graph(
+ node_label=node_label, max_depth=max_depth, max_nodes=max_nodes
+ )
+
+ return self._convert_lightrag_graph(raw_graph)
+
+ async def add_entity(self, triples: list[dict], **kwargs) -> bool:
+ """
+ LightRAG typically builds graph from text.
+ Direct triple injection might not be supported or requires different API.
+ """
+ logger.warning("add_entity is not fully supported for LightRAG adapter yet.")
+ return False
+
+ async def get_sample_nodes(self, num: int = 50, **kwargs) -> dict[str, list]:
+ # Use query_nodes with wildcard to get a subgraph
+ return await self.query_nodes("*", max_nodes=num, **kwargs)
+
+ def normalize_node(self, raw_node: Any) -> dict[str, Any]:
+ # Handle LightRAG Node object
+ node_id = getattr(raw_node, "id", None)
+ if node_id is None:
+ node_id = raw_node.get("id")
+
+ labels = getattr(raw_node, "labels", [])
+ if not labels and hasattr(raw_node, "get"):
+ labels = raw_node.get("labels", [])
+
+ properties = getattr(raw_node, "properties", {})
+ if not properties and hasattr(raw_node, "get"):
+ properties = raw_node.get("properties", {})
+
+ # 优先使用 entity_id 作为显示名称,因为 Neo4j 中 LightRAG 存储的实体名称在 entity_id 字段
+ # 如果不存在,则回退到 id
+ name = properties.get("entity_id", node_id)
+
+ # 尝试从 properties 获取 entity_type,或者从 labels 中推断(排除 kb_ 前缀的 label)
+ entity_type = properties.get("entity_type", "unknown")
+ if entity_type == "unknown" and labels:
+ for label in labels:
+ if not label.startswith("kb_"):
+ entity_type = label
+ break
+
+ return self._create_standard_node(
+ node_id=node_id,
+ name=name,
+ entity_type=entity_type,
+ labels=labels,
+ properties=properties,
+ source="lightrag"
+ )
+
+ def normalize_edge(self, raw_edge: Any) -> dict[str, Any]:
+ # Handle LightRAG Edge object
+ edge_id = getattr(raw_edge, "id", None)
+ if edge_id is None:
+ edge_id = raw_edge.get("id")
+
+ source = getattr(raw_edge, "source", None)
+ if source is None:
+ source = raw_edge.get("source")
+
+ target = getattr(raw_edge, "target", None)
+ if target is None:
+ target = raw_edge.get("target")
+
+ edge_type = getattr(raw_edge, "type", None)
+ if edge_type is None:
+ edge_type = raw_edge.get("type")
+
+ properties = getattr(raw_edge, "properties", {})
+ if not properties and hasattr(raw_edge, "get"):
+ properties = raw_edge.get("properties", {})
+
+ # 优化边的显示类型
+ # LightRAG 的边类型通常是 "DIRECTED",具体含义在 keywords 或 description 中
+ display_type = edge_type
+ if edge_type == "DIRECTED":
+ keywords = properties.get("keywords", [])
+ if keywords and isinstance(keywords, list) and len(keywords) > 0:
+ display_type = keywords[0]
+ elif properties.get("description"):
+ # 如果没有 keywords,尝试从 description 截取(太长就算了)
+ desc = properties.get("description", "")
+ if len(desc) < 20:
+ display_type = desc
+ else:
+ display_type = "related" # fallback
+
+ return self._create_standard_edge(
+ edge_id=edge_id,
+ source_id=source,
+ target_id=target,
+ edge_type=display_type,
+ properties=properties
+ )
+
+ async def get_labels(self) -> list[str]:
+ return await self.lightrag.get_graph_labels()
+
+ def _convert_lightrag_graph(self, raw_graph) -> dict[str, Any]:
+ nodes = []
+ edges = []
+
+ # raw_graph has .nodes and .edges lists
+ if hasattr(raw_graph, "nodes"):
+ for node in raw_graph.nodes:
+ nodes.append(self.normalize_node(node))
+
+ if hasattr(raw_graph, "edges"):
+ for edge in raw_graph.edges:
+ edges.append(self.normalize_edge(edge))
+
+ result = {"nodes": nodes, "edges": edges}
+
+ # Add metadata if available
+ if hasattr(raw_graph, "is_truncated"):
+ result["is_truncated"] = raw_graph.is_truncated
+
+ result["total_nodes"] = len(nodes)
+ result["total_edges"] = len(edges)
+
+ return result
diff --git a/src/knowledge/adapters/upload.py b/src/knowledge/adapters/upload.py
new file mode 100644
index 00000000..a46188f1
--- /dev/null
+++ b/src/knowledge/adapters/upload.py
@@ -0,0 +1,112 @@
+from typing import Any
+
+from src.knowledge.graph import GraphDatabase
+
+from .base import GraphAdapter
+
+
+class UploadGraphAdapter(GraphAdapter):
+ """Upload图谱适配器 (Upload Graph Adapter)"""
+
+ def __init__(self, graph_db_instance: GraphDatabase, config: dict[str, Any] = None):
+ self.config = config or {
+ "node_label": "Entity:Upload",
+ "id_field": "name",
+ "relation_label": "RELATION",
+ "default_tags": ["upload", "user_generated"],
+ }
+ self.graph_db = graph_db_instance
+
+ async def query_nodes(self, keyword: str, **kwargs) -> dict[str, Any]:
+ params = self._normalize_query_params(keyword, kwargs)
+
+ # 如果关键词是 "*" 或者为空,则执行采样查询
+ if not params["keyword"] or params["keyword"] == "*":
+ # 映射 max_nodes 到 num
+ num = kwargs.get("max_nodes", 100)
+ raw_results = self.graph_db.get_sample_nodes(
+ kgdb_name=params.get("kgdb_name", "neo4j"),
+ num=num
+ )
+ else:
+ # 否则执行关键词搜索
+ # graph_db.query_node is sync
+ raw_results = self.graph_db.query_node(
+ keyword=params["keyword"],
+ threshold=params.get("threshold", 0.9),
+ kgdb_name=params.get("kgdb_name", "neo4j"),
+ hops=params.get("hops", 2),
+ return_format="graph",
+ )
+
+ return self._format_results(raw_results)
+
+ async def add_entity(self, triples: list[dict], **kwargs) -> bool:
+ kgdb_name = kwargs.get("kgdb_name", "neo4j")
+ # txt_add_vector_entity is async
+ await self.graph_db.txt_add_vector_entity(triples, kgdb_name=kgdb_name)
+ return True
+
+ async def get_sample_nodes(self, num: int = 50, **kwargs) -> dict[str, list]:
+ kgdb_name = kwargs.get("kgdb_name", "neo4j")
+ # get_sample_nodes is sync
+ raw_results = self.graph_db.get_sample_nodes(kgdb_name=kgdb_name, num=num)
+ return self._format_results(raw_results)
+
+ def normalize_node(self, raw_node: Any) -> dict[str, Any]:
+ """
+ raw_node expected format: {id: str, name: str, ...}
+ """
+ node_id = raw_node.get("id")
+ name = raw_node.get("name")
+
+ return self._create_standard_node(
+ node_id=node_id,
+ name=name,
+ entity_type="entity",
+ labels=["Entity", "Upload"],
+ properties=raw_node,
+ source="upload"
+ )
+
+ def normalize_edge(self, raw_edge: Any) -> dict[str, Any]:
+ """
+ raw_edge expected format: {type: str, source_id: str, target_id: str, ...}
+ """
+ # Generate an ID if not present (Upload graph edges might not have explicit ID in simple dict return)
+ edge_id = raw_edge.get("id")
+ if not edge_id:
+ edge_id = f"{raw_edge.get('source_id')}_{raw_edge.get('type')}_{raw_edge.get('target_id')}"
+
+ return self._create_standard_edge(
+ edge_id=edge_id,
+ source_id=raw_edge.get("source_id"),
+ target_id=raw_edge.get("target_id"),
+ edge_type=raw_edge.get("type"),
+ properties=raw_edge
+ )
+
+ async def get_labels(self) -> list[str]:
+ kgdb_name = self.config.get("kgdb_name", "neo4j")
+ info = self.graph_db.get_graph_info(graph_name=kgdb_name)
+ return info.get("labels", []) if info else []
+
+ def _normalize_query_params(self, keyword: str, kwargs: dict) -> dict[str, Any]:
+ # Map max_depth to hops if present
+ hops = kwargs.get("hops")
+ if hops is None:
+ hops = kwargs.get("max_depth", 2)
+
+ return {
+ "keyword": keyword,
+ "threshold": kwargs.get("threshold", 0.9),
+ "kgdb_name": kwargs.get("kgdb_name", "neo4j"),
+ "hops": hops,
+ "filters": kwargs.get("filters", {}),
+ "context": kwargs.get("context", {}),
+ }
+
+ def _format_results(self, raw_results: dict[str, list]) -> dict[str, list]:
+ nodes = [self.normalize_node(n) for n in raw_results.get("nodes", [])]
+ edges = [self.normalize_edge(e) for e in raw_results.get("edges", [])]
+ return {"nodes": nodes, "edges": edges}
diff --git a/src/knowledge/graph.py b/src/knowledge/graph.py
index 4b611f18..61e2c85a 100644
--- a/src/knowledge/graph.py
+++ b/src/knowledge/graph.py
@@ -9,7 +9,7 @@ from neo4j import GraphDatabase as GD
from src import config
from src.models import select_embedding_model
-from src.storage.minio.client import get_minio_client, StorageError
+from src.storage.minio.client import get_minio_client
from src.utils import logger
from src.utils.datetime_utils import utc_isoformat
@@ -106,7 +106,12 @@ class GraphDatabase:
RETURN
{id: elementId(n), name: n.name} AS h,
CASE WHEN rel IS NOT NULL THEN
- {type: rel.type, source_id: elementId(n), target_id: elementId(m)}
+ {
+ id: elementId(rel),
+ type: rel.type,
+ source_id: elementId(startNode(rel)),
+ target_id: elementId(endNode(rel))
+ }
ELSE null END AS r,
CASE WHEN m IS NOT NULL THEN
{id: elementId(m), name: m.name}
@@ -166,7 +171,12 @@ class GraphDatabase:
WHERE elementId(n) < elementId(m)
RETURN
{id: elementId(n), name: n.name} AS h,
- {type: r.type, source_id: elementId(n), target_id: elementId(m)} AS r,
+ {
+ id: elementId(r),
+ type: r.type,
+ source_id: elementId(startNode(r)),
+ target_id: elementId(endNode(r))
+ } AS r,
{id: elementId(m), name: m.name} AS t
LIMIT $num
"""
@@ -355,12 +365,12 @@ class GraphDatabase:
temp_file_path = None
try:
- if parsed_url.scheme in ('http', 'https'): # 如果是 URL
+ if parsed_url.scheme in ("http", "https"): # 如果是 URL
logger.info(f"检测到 URL,正在从 MinIO 下载文件: {file_path}")
# 从 URL 解析 bucket_name 和 object_name
# URL 格式: http://host:port/bucket_name/object_name
- path_parts = parsed_url.path.lstrip('/').split('/', 1)
+ path_parts = parsed_url.path.lstrip("/").split("/", 1)
if len(path_parts) < 2:
raise ValueError(f"无法解析 MinIO URL: {file_path}")
@@ -372,8 +382,10 @@ class GraphDatabase:
file_data = await minio_client.adownload_file(bucket_name, object_name)
# 创建临时文件保存下载的内容
- with tempfile.NamedTemporaryFile(mode='w', suffix='.jsonl', delete=False, encoding='utf-8') as temp_file:
- temp_file.write(file_data.decode('utf-8'))
+ with tempfile.NamedTemporaryFile(
+ mode="w", suffix=".jsonl", delete=False, encoding="utf-8"
+ ) as temp_file:
+ temp_file.write(file_data.decode("utf-8"))
temp_file_path = temp_file.name
logger.info(f"文件已下载到临时路径: {temp_file_path}")
@@ -592,22 +604,22 @@ class GraphDatabase:
// 1跳出边
[(n {name: $entity_name})-[r1]->(m1) |
{h: {id: elementId(n), name: n.name},
- r: {type: r1.type, source_id: elementId(n), target_id: elementId(m1)},
+ r: {id: elementId(r1), type: r1.type, source_id: elementId(n), target_id: elementId(m1)},
t: {id: elementId(m1), name: m1.name}}],
// 2跳出边
[(n {name: $entity_name})-[r1]->(m1)-[r2]->(m2) |
{h: {id: elementId(m1), name: m1.name},
- r: {type: r2.type, source_id: elementId(m1), target_id: elementId(m2)},
+ r: {id: elementId(r2), type: r2.type, source_id: elementId(m1), target_id: elementId(m2)},
t: {id: elementId(m2), name: m2.name}}],
// 1跳入边
[(m1)-[r1]->(n {name: $entity_name}) |
{h: {id: elementId(m1), name: m1.name},
- r: {type: r1.type, source_id: elementId(m1), target_id: elementId(n)},
+ r: {id: elementId(r1), type: r1.type, source_id: elementId(m1), target_id: elementId(n)},
t: {id: elementId(n), name: n.name}}],
// 2跳入边
[(m2)-[r2]->(m1)-[r1]->(n {name: $entity_name}) |
{h: {id: elementId(m2), name: m2.name},
- r: {type: r2.type, source_id: elementId(m2), target_id: elementId(m1)},
+ r: {id: elementId(r2), type: r2.type, source_id: elementId(m2), target_id: elementId(m1)},
t: {id: elementId(m1), name: m1.name}}]
] AS all_results
UNWIND all_results AS result_list
diff --git a/src/knowledge/utils/kb_utils.py b/src/knowledge/utils/kb_utils.py
index d955fffd..75dbec58 100644
--- a/src/knowledge/utils/kb_utils.py
+++ b/src/knowledge/utils/kb_utils.py
@@ -319,7 +319,7 @@ def get_embedding_config(embed_info: dict) -> dict:
"model": embed_info["name"],
"api_key": os.getenv(embed_info["api_key"]) or embed_info["api_key"],
"base_url": embed_info["base_url"],
- "dimension": embed_info.get("dimension", 1024)
+ "dimension": embed_info.get("dimension", 1024),
}
logger.debug(f"Embedding config from dict: {config_dict}")
return config_dict
diff --git a/test/api/test_unified_graph_router.py b/test/api/test_unified_graph_router.py
new file mode 100644
index 00000000..fa23c916
--- /dev/null
+++ b/test/api/test_unified_graph_router.py
@@ -0,0 +1,138 @@
+"""
+Integration tests for the unified graph router endpoints.
+"""
+
+from __future__ import annotations
+
+import pytest
+
+pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
+
+
+async def test_get_graphs_list(test_client, admin_headers):
+ """Test retrieving the list of all graphs."""
+ response = await test_client.get("/api/graph/list", headers=admin_headers)
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["success"] is True
+ graphs = payload["data"]
+ assert isinstance(graphs, list)
+
+ # Check for Neo4j default graph
+ neo4j_graph = next((g for g in graphs if g["id"] == "neo4j"), None)
+ assert neo4j_graph is not None
+ assert neo4j_graph["type"] == "neo4j"
+
+ # Note: LightRAG graphs might be empty if none created, but we check structure
+
+
+async def test_get_subgraph_neo4j(test_client, admin_headers):
+ """Test unified subgraph query for Neo4j."""
+ # Query with a wildcard or a known node. Using "*" to get a sample.
+ response = await test_client.get(
+ "/api/graph/subgraph",
+ params={"db_id": "neo4j", "node_label": "*", "max_nodes": 10},
+ headers=admin_headers
+ )
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["success"] is True
+ data = payload["data"]
+ assert "nodes" in data
+ assert "edges" in data
+ assert isinstance(data["nodes"], list)
+
+
+async def test_get_subgraph_lightrag(test_client, admin_headers, knowledge_database):
+ """Test unified subgraph query for LightRAG."""
+ db_id = knowledge_database["db_id"]
+ response = await test_client.get(
+ "/api/graph/subgraph",
+ params={"db_id": db_id, "node_label": "*", "max_nodes": 10},
+ headers=admin_headers
+ )
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["success"] is True
+ data = payload["data"]
+ assert "nodes" in data
+ assert "edges" in data
+
+
+async def test_get_stats_neo4j(test_client, admin_headers):
+ """Test stats endpoint for Neo4j."""
+ response = await test_client.get(
+ "/api/graph/stats",
+ params={"db_id": "neo4j"},
+ headers=admin_headers
+ )
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["success"] is True
+ data = payload["data"]
+ assert "total_nodes" in data
+ assert "total_edges" in data
+ assert "entity_types" in data
+
+
+async def test_get_stats_lightrag(test_client, admin_headers, knowledge_database):
+ """Test stats endpoint for LightRAG."""
+ db_id = knowledge_database["db_id"]
+ response = await test_client.get(
+ "/api/graph/stats",
+ params={"db_id": db_id},
+ headers=admin_headers
+ )
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["success"] is True
+ data = payload["data"]
+ assert "total_nodes" in data
+ assert "total_edges" in data
+ assert "entity_types" in data
+
+
+async def test_get_labels_neo4j(test_client, admin_headers):
+ """Test labels endpoint for Neo4j."""
+ response = await test_client.get(
+ "/api/graph/labels",
+ params={"db_id": "neo4j"},
+ headers=admin_headers
+ )
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["success"] is True
+ data = payload["data"]
+ assert "labels" in data
+ assert isinstance(data["labels"], list)
+
+
+async def test_deprecated_neo4j_endpoints(test_client, admin_headers):
+ """Verify deprecated endpoints still work and return correct structure."""
+
+ # /neo4j/nodes
+ response = await test_client.get(
+ "/api/graph/neo4j/nodes",
+ params={"kgdb_name": "neo4j", "num": 5},
+ headers=admin_headers
+ )
+ assert response.status_code == 200
+ payload = response.json()
+ # Check compatibility structure
+ assert payload["success"] is True
+ assert "result" in payload
+ assert payload["message"] == "success"
+ assert "nodes" in payload["result"]
+
+ # /neo4j/node
+ # This might return empty if "NonExistentEntity" doesn't exist, but structure should be valid
+ response = await test_client.get(
+ "/api/graph/neo4j/node",
+ params={"entity_name": "NonExistentEntity"},
+ headers=admin_headers
+ )
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["success"] is True
+ assert "result" in payload
+ assert payload["message"] == "success"
diff --git a/web/src/apis/graph_api.js b/web/src/apis/graph_api.js
index aed00c9a..be5b67d1 100644
--- a/web/src/apis/graph_api.js
+++ b/web/src/apis/graph_api.js
@@ -7,15 +7,23 @@ import { apiGet, apiPost } from './base'
*/
// =============================================================================
-// === LightRAG图知识库接口分组 ===
+// === 统一图谱接口 (Unified Graph API) ===
// =============================================================================
-export const lightragApi = {
+export const unifiedApi = {
/**
- * 获取LightRAG知识图谱子图数据
+ * 获取所有可用的知识图谱列表
+ * @returns {Promise} - 图谱列表
+ */
+ getGraphs: async () => {
+ return await apiGet('/api/graph/list', {}, true)
+ },
+
+ /**
+ * 获取子图数据 (统一接口)
* @param {Object} params - 查询参数
- * @param {string} params.db_id - LightRAG数据库ID
- * @param {string} params.node_label - 节点标签("*"获取全图)
+ * @param {string} params.db_id - 图谱ID
+ * @param {string} params.node_label - 节点标签/关键词
* @param {number} params.max_depth - 最大深度
* @param {number} params.max_nodes - 最大节点数
* @returns {Promise} - 子图数据
@@ -34,37 +42,12 @@ export const lightragApi = {
max_nodes: max_nodes.toString()
})
- return await apiGet(`/api/graph/lightrag/subgraph?${queryParams.toString()}`, {}, true)
+ return await apiGet(`/api/graph/subgraph?${queryParams.toString()}`, {}, true)
},
/**
- * 获取所有可用的LightRAG数据库
- * @returns {Promise} - LightRAG数据库列表
- */
- getDatabases: async () => {
- return await apiGet('/api/graph/lightrag/databases', {}, true)
- },
-
- /**
- * 获取LightRAG图谱标签列表
- * @param {string} db_id - LightRAG数据库ID
- * @returns {Promise} - 标签列表
- */
- getLabels: async (db_id) => {
- if (!db_id) {
- throw new Error('db_id is required')
- }
-
- const queryParams = new URLSearchParams({
- db_id: db_id
- })
-
- return await apiGet(`/api/graph/lightrag/labels?${queryParams.toString()}`, {}, true)
- },
-
- /**
- * 获取LightRAG图谱统计信息
- * @param {string} db_id - LightRAG数据库ID
+ * 获取图谱统计信息 (统一接口)
+ * @param {string} db_id - 图谱ID
* @returns {Promise} - 统计信息
*/
getStats: async (db_id) => {
@@ -76,10 +59,28 @@ export const lightragApi = {
db_id: db_id
})
- return await apiGet(`/api/graph/lightrag/stats?${queryParams.toString()}`, {}, true)
+ return await apiGet(`/api/graph/stats?${queryParams.toString()}`, {}, true)
+ },
+
+ /**
+ * 获取图谱标签列表 (统一接口)
+ * @param {string} db_id - 图谱ID
+ * @returns {Promise} - 标签列表
+ */
+ getLabels: async (db_id) => {
+ if (!db_id) {
+ throw new Error('db_id is required')
+ }
+
+ const queryParams = new URLSearchParams({
+ db_id: db_id
+ })
+
+ return await apiGet(`/api/graph/labels?${queryParams.toString()}`, {}, true)
}
}
+
// =============================================================================
// === Neo4j图数据库接口分组 ===
// =============================================================================
@@ -222,11 +223,21 @@ export const getGraphStats = async () => {
return neo4jApi.getInfo()
}
-// 保持旧的分组导出,便于批量替换
+// 兼容性导出 - 使用统一接口替代旧有的 graphApi
export const graphApi = {
- getSubgraph: lightragApi.getSubgraph,
- getDatabases: lightragApi.getDatabases,
- getLabels: lightragApi.getLabels,
- getStats: lightragApi.getStats,
- ...neo4jApi // 临时兼容
+ // 使用统一接口替代 LightRAG 接口
+ getSubgraph: unifiedApi.getSubgraph,
+ getDatabases: async () => {
+ // 使用统一接口获取所有图谱,然后过滤出 LightRAG 类型的
+ const response = await unifiedApi.getGraphs()
+ if (response.success) {
+ const lightragDbs = response.data.filter(graph => graph.type === 'lightrag')
+ return { success: true, data: { databases: lightragDbs } }
+ }
+ return response
+ },
+ getLabels: unifiedApi.getLabels,
+ getStats: unifiedApi.getStats,
+ // 保留 Neo4j 接口
+ ...neo4jApi
}
\ No newline at end of file
diff --git a/web/src/components/GraphCanvas.vue b/web/src/components/GraphCanvas.vue
index 0997c478..ba880cfd 100644
--- a/web/src/components/GraphCanvas.vue
+++ b/web/src/components/GraphCanvas.vue
@@ -37,7 +37,7 @@ const props = defineProps({
highlightKeywords: { type: Array, default: () => [] }
})
-const emit = defineEmits(['ready', 'data-rendered'])
+const emit = defineEmits(['ready', 'data-rendered', 'node-click', 'edge-click', 'canvas-click'])
const container = ref(null)
const rootEl = ref(null)
@@ -88,6 +88,7 @@ function formatData() {
data: {
label: n[props.labelField] ?? n.name ?? String(n.id),
degree: degrees.get(String(n.id)) || 0,
+ original: n // 保存原始数据
},
}))
@@ -95,7 +96,10 @@ function formatData() {
id: e.id ? String(e.id) : `edge-${idx}`,
source: String(e.source_id),
target: String(e.target_id),
- data: { label: e.type ?? '' },
+ data: {
+ label: e.type ?? '',
+ original: e // 保存原始数据
+ },
}))
return { nodes, edges }
@@ -185,9 +189,38 @@ function initGraph() {
unselectedState: 'inactive', // 未选中节点状态
multiple: true,
trigger: ['shift'],
+ // 禁用默认的选中效果,避免与自定义事件冲突
+ disableDefault: false,
}
],
})
+
+ // 绑定事件
+ graphInstance.on('node:click', (evt) => {
+ const { target } = evt
+ // 获取节点ID
+ const nodeId = target.id
+ // 从 graph data 中找到对应的节点数据
+ // 注意:G6 v5 的事件对象结构可能有所不同,这里假设 target.id 是节点ID
+ // 也可以通过 graphInstance.getNodeData(nodeId) 获取
+ const nodeData = graphInstance.getNodeData(nodeId)
+ emit('node-click', nodeData)
+ })
+
+ graphInstance.on('edge:click', (evt) => {
+ const { target } = evt
+ const edgeId = target.id
+ const edgeData = graphInstance.getEdgeData(edgeId)
+ emit('edge-click', edgeData)
+ })
+
+ graphInstance.on('canvas:click', (evt) => {
+ // 只有点击画布空白处才触发
+ if (!evt.target) {
+ emit('canvas-click')
+ }
+ })
+
emit('ready', graphInstance)
}
diff --git a/web/src/components/GraphDetailPanel.vue b/web/src/components/GraphDetailPanel.vue
new file mode 100644
index 00000000..4df34962
--- /dev/null
+++ b/web/src/components/GraphDetailPanel.vue
@@ -0,0 +1,167 @@
+
+
只有 LightRAG 类型的知识库支持知识图谱。
-本页面展示的是 Neo4j 图数据库中的知识图谱信息。
具体展示内容包括:
注意:
graphbase.py 中的 get_sample_nodes 方法实现:graphbase.py 中的 get_sample_nodes 方法实现。MATCH (n:Entity)-[r]->(m:Entity)
-RETURN
- {id: elementId(n), name: n.name} AS h,
- {type: r.type, source_id: elementId(n), target_id: elementId(m)} AS r,
- {id: elementId(m), name: m.name} AS t
-LIMIT $num
- 如需查看完整的 Neo4j 数据库内容,请使用 "Neo4j 浏览器" 按钮访问原生界面。
+本页面展示的是 LightRAG 知识库生成的图谱信息。
+数据来源于选定的知识库实例。
+支持通过实体名称进行模糊搜索,输入 "*" 可查看采样全图。