mirror of https://github.com/Mai-with-u/MaiBot.git
Merge branch 'dev' of https://github.com/Mai-with-u/MaiBot into dev
commit
e0b28f9708
|
|
@ -0,0 +1,312 @@
|
|||
"""知识库图谱可视化 API 路由"""
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Query
|
||||
from pydantic import BaseModel
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/webui/knowledge", tags=["knowledge"])
|
||||
|
||||
|
||||
class KnowledgeNode(BaseModel):
|
||||
"""知识节点"""
|
||||
id: str
|
||||
type: str # 'entity' or 'paragraph'
|
||||
content: str
|
||||
create_time: Optional[float] = None
|
||||
|
||||
|
||||
class KnowledgeEdge(BaseModel):
|
||||
"""知识边"""
|
||||
source: str
|
||||
target: str
|
||||
weight: float
|
||||
create_time: Optional[float] = None
|
||||
update_time: Optional[float] = None
|
||||
|
||||
|
||||
class KnowledgeGraph(BaseModel):
|
||||
"""知识图谱"""
|
||||
nodes: List[KnowledgeNode]
|
||||
edges: List[KnowledgeEdge]
|
||||
|
||||
|
||||
class KnowledgeStats(BaseModel):
|
||||
"""知识库统计信息"""
|
||||
total_nodes: int
|
||||
total_edges: int
|
||||
entity_nodes: int
|
||||
paragraph_nodes: int
|
||||
avg_connections: float
|
||||
|
||||
|
||||
def _load_kg_manager():
|
||||
"""延迟加载 KGManager"""
|
||||
try:
|
||||
from src.chat.knowledge.kg_manager import KGManager
|
||||
|
||||
kg_manager = KGManager()
|
||||
kg_manager.load_from_file()
|
||||
return kg_manager
|
||||
except Exception as e:
|
||||
logger.error(f"加载 KGManager 失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _convert_graph_to_json(kg_manager) -> KnowledgeGraph:
|
||||
"""将 DiGraph 转换为 JSON 格式"""
|
||||
if kg_manager is None or kg_manager.graph is None:
|
||||
return KnowledgeGraph(nodes=[], edges=[])
|
||||
|
||||
graph = kg_manager.graph
|
||||
nodes = []
|
||||
edges = []
|
||||
|
||||
# 转换节点
|
||||
node_list = graph.get_node_list()
|
||||
for node_id in node_list:
|
||||
try:
|
||||
node_data = graph[node_id]
|
||||
# 节点类型: "ent" -> "entity", "pg" -> "paragraph"
|
||||
node_type = "entity" if ('type' in node_data and node_data['type'] == 'ent') else "paragraph"
|
||||
content = node_data['content'] if 'content' in node_data else node_id
|
||||
create_time = node_data['create_time'] if 'create_time' in node_data else None
|
||||
|
||||
nodes.append(KnowledgeNode(
|
||||
id=node_id,
|
||||
type=node_type,
|
||||
content=content,
|
||||
create_time=create_time
|
||||
))
|
||||
except Exception as e:
|
||||
logger.warning(f"跳过节点 {node_id}: {e}")
|
||||
continue
|
||||
|
||||
# 转换边
|
||||
edge_list = graph.get_edge_list()
|
||||
for edge_tuple in edge_list:
|
||||
try:
|
||||
# edge_tuple 是 (source, target) 元组
|
||||
source, target = edge_tuple[0], edge_tuple[1]
|
||||
# 通过 graph[source, target] 获取边的属性数据
|
||||
edge_data = graph[source, target]
|
||||
|
||||
# edge_data 支持 [] 操作符但不支持 .get()
|
||||
weight = edge_data['weight'] if 'weight' in edge_data else 1.0
|
||||
create_time = edge_data['create_time'] if 'create_time' in edge_data else None
|
||||
update_time = edge_data['update_time'] if 'update_time' in edge_data else None
|
||||
|
||||
edges.append(KnowledgeEdge(
|
||||
source=source,
|
||||
target=target,
|
||||
weight=weight,
|
||||
create_time=create_time,
|
||||
update_time=update_time
|
||||
))
|
||||
except Exception as e:
|
||||
logger.warning(f"跳过边 {edge_tuple}: {e}")
|
||||
continue
|
||||
|
||||
return KnowledgeGraph(nodes=nodes, edges=edges)
|
||||
|
||||
|
||||
@router.get("/graph", response_model=KnowledgeGraph)
|
||||
async def get_knowledge_graph(
|
||||
limit: int = Query(100, ge=1, le=10000, description="返回的最大节点数"),
|
||||
node_type: str = Query("all", description="节点类型过滤: all, entity, paragraph")
|
||||
):
|
||||
"""获取知识图谱(限制节点数量)
|
||||
|
||||
Args:
|
||||
limit: 返回的最大节点数,默认 100,最大 10000
|
||||
node_type: 节点类型过滤 - all(全部), entity(实体), paragraph(段落)
|
||||
|
||||
Returns:
|
||||
KnowledgeGraph: 包含指定数量节点和相关边的知识图谱
|
||||
"""
|
||||
try:
|
||||
kg_manager = _load_kg_manager()
|
||||
if kg_manager is None:
|
||||
logger.warning("KGManager 未初始化,返回空图谱")
|
||||
return KnowledgeGraph(nodes=[], edges=[])
|
||||
|
||||
graph = kg_manager.graph
|
||||
all_node_list = graph.get_node_list()
|
||||
|
||||
# 按类型过滤节点
|
||||
if node_type == "entity":
|
||||
all_node_list = [n for n in all_node_list if n in graph and 'type' in graph[n] and graph[n]['type'] == 'ent']
|
||||
elif node_type == "paragraph":
|
||||
all_node_list = [n for n in all_node_list if n in graph and 'type' in graph[n] and graph[n]['type'] == 'pg']
|
||||
|
||||
# 限制节点数量
|
||||
total_nodes = len(all_node_list)
|
||||
if len(all_node_list) > limit:
|
||||
node_list = all_node_list[:limit]
|
||||
else:
|
||||
node_list = all_node_list
|
||||
|
||||
logger.info(f"总节点数: {total_nodes}, 返回节点: {len(node_list)} (limit={limit}, type={node_type})")
|
||||
|
||||
# 转换节点
|
||||
nodes = []
|
||||
node_ids = set()
|
||||
for node_id in node_list:
|
||||
try:
|
||||
node_data = graph[node_id]
|
||||
node_type_val = "entity" if ('type' in node_data and node_data['type'] == 'ent') else "paragraph"
|
||||
content = node_data['content'] if 'content' in node_data else node_id
|
||||
create_time = node_data['create_time'] if 'create_time' in node_data else None
|
||||
|
||||
nodes.append(KnowledgeNode(
|
||||
id=node_id,
|
||||
type=node_type_val,
|
||||
content=content,
|
||||
create_time=create_time
|
||||
))
|
||||
node_ids.add(node_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"跳过节点 {node_id}: {e}")
|
||||
continue
|
||||
|
||||
# 只获取涉及当前节点集的边(保证图的完整性)
|
||||
edges = []
|
||||
edge_list = graph.get_edge_list()
|
||||
for edge_tuple in edge_list:
|
||||
try:
|
||||
source, target = edge_tuple[0], edge_tuple[1]
|
||||
# 只包含两端都在当前节点集中的边
|
||||
if source not in node_ids or target not in node_ids:
|
||||
continue
|
||||
|
||||
edge_data = graph[source, target]
|
||||
weight = edge_data['weight'] if 'weight' in edge_data else 1.0
|
||||
create_time = edge_data['create_time'] if 'create_time' in edge_data else None
|
||||
update_time = edge_data['update_time'] if 'update_time' in edge_data else None
|
||||
|
||||
edges.append(KnowledgeEdge(
|
||||
source=source,
|
||||
target=target,
|
||||
weight=weight,
|
||||
create_time=create_time,
|
||||
update_time=update_time
|
||||
))
|
||||
except Exception as e:
|
||||
logger.warning(f"跳过边 {edge_tuple}: {e}")
|
||||
continue
|
||||
|
||||
graph_data = KnowledgeGraph(nodes=nodes, edges=edges)
|
||||
logger.info(f"返回知识图谱: {len(nodes)} 个节点, {len(edges)} 条边")
|
||||
return graph_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取知识图谱失败: {e}", exc_info=True)
|
||||
return KnowledgeGraph(nodes=[], edges=[])
|
||||
|
||||
|
||||
@router.get("/stats", response_model=KnowledgeStats)
|
||||
async def get_knowledge_stats():
|
||||
"""获取知识库统计信息
|
||||
|
||||
Returns:
|
||||
KnowledgeStats: 统计信息
|
||||
"""
|
||||
try:
|
||||
kg_manager = _load_kg_manager()
|
||||
if kg_manager is None or kg_manager.graph is None:
|
||||
return KnowledgeStats(
|
||||
total_nodes=0,
|
||||
total_edges=0,
|
||||
entity_nodes=0,
|
||||
paragraph_nodes=0,
|
||||
avg_connections=0.0
|
||||
)
|
||||
|
||||
graph = kg_manager.graph
|
||||
node_list = graph.get_node_list()
|
||||
edge_list = graph.get_edge_list()
|
||||
|
||||
total_nodes = len(node_list)
|
||||
total_edges = len(edge_list)
|
||||
|
||||
# 统计节点类型
|
||||
entity_nodes = 0
|
||||
paragraph_nodes = 0
|
||||
for node_id in node_list:
|
||||
try:
|
||||
node_data = graph[node_id]
|
||||
node_type = node_data['type'] if 'type' in node_data else 'ent'
|
||||
if node_type == 'ent':
|
||||
entity_nodes += 1
|
||||
elif node_type == 'pg':
|
||||
paragraph_nodes += 1
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# 计算平均连接数
|
||||
avg_connections = (total_edges * 2) / total_nodes if total_nodes > 0 else 0.0
|
||||
|
||||
return KnowledgeStats(
|
||||
total_nodes=total_nodes,
|
||||
total_edges=total_edges,
|
||||
entity_nodes=entity_nodes,
|
||||
paragraph_nodes=paragraph_nodes,
|
||||
avg_connections=round(avg_connections, 2)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取统计信息失败: {e}", exc_info=True)
|
||||
return KnowledgeStats(
|
||||
total_nodes=0,
|
||||
total_edges=0,
|
||||
entity_nodes=0,
|
||||
paragraph_nodes=0,
|
||||
avg_connections=0.0
|
||||
)
|
||||
|
||||
|
||||
@router.get("/search", response_model=List[KnowledgeNode])
|
||||
async def search_knowledge_node(query: str = Query(..., min_length=1)):
|
||||
"""搜索知识节点
|
||||
|
||||
Args:
|
||||
query: 搜索关键词
|
||||
|
||||
Returns:
|
||||
List[KnowledgeNode]: 匹配的节点列表
|
||||
"""
|
||||
try:
|
||||
kg_manager = _load_kg_manager()
|
||||
if kg_manager is None or kg_manager.graph is None:
|
||||
return []
|
||||
|
||||
graph = kg_manager.graph
|
||||
node_list = graph.get_node_list()
|
||||
results = []
|
||||
query_lower = query.lower()
|
||||
|
||||
# 在节点内容中搜索
|
||||
for node_id in node_list:
|
||||
try:
|
||||
node_data = graph[node_id]
|
||||
content = node_data['content'] if 'content' in node_data else node_id
|
||||
node_type = "entity" if ('type' in node_data and node_data['type'] == 'ent') else "paragraph"
|
||||
|
||||
if query_lower in content.lower() or query_lower in node_id.lower():
|
||||
create_time = node_data['create_time'] if 'create_time' in node_data else None
|
||||
results.append(KnowledgeNode(
|
||||
id=node_id,
|
||||
type=node_type,
|
||||
content=content,
|
||||
create_time=create_time
|
||||
))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
logger.info(f"搜索 '{query}' 找到 {len(results)} 个节点")
|
||||
return results[:50] # 限制返回数量
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"搜索节点失败: {e}", exc_info=True)
|
||||
return []
|
||||
|
|
@ -42,19 +42,23 @@ async def restart_maibot():
|
|||
使用 os.execv 重启当前进程,配置更改将在重启后生效。
|
||||
注意:此操作会使麦麦暂时离线。
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
# 记录重启操作
|
||||
print(f"[{datetime.now()}] WebUI 触发重启操作")
|
||||
|
||||
# 使用 os.execv 重启当前进程
|
||||
# 这会替换当前进程,保持相同的 PID
|
||||
python = sys.executable
|
||||
args = [python] + sys.argv
|
||||
|
||||
# 返回成功响应(实际上这个响应可能不会发送,因为进程会立即重启)
|
||||
# 但我们仍然返回它以保持 API 一致性
|
||||
os.execv(python, args)
|
||||
# 定义延迟重启的异步任务
|
||||
async def delayed_restart():
|
||||
await asyncio.sleep(0.5) # 延迟0.5秒,确保响应已发送
|
||||
python = sys.executable
|
||||
args = [python] + sys.argv
|
||||
os.execv(python, args)
|
||||
|
||||
# 创建后台任务执行重启
|
||||
asyncio.create_task(delayed_restart())
|
||||
|
||||
# 立即返回成功响应
|
||||
return RestartResponse(success=True, message="麦麦正在重启中...")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"重启失败: {str(e)}") from e
|
||||
|
|
|
|||
|
|
@ -88,14 +88,20 @@ class WebUIServer:
|
|||
# 导入所有 WebUI 路由
|
||||
from src.webui.routes import router as webui_router
|
||||
from src.webui.logs_ws import router as logs_router
|
||||
|
||||
logger.info("开始导入 knowledge_routes...")
|
||||
from src.webui.knowledge_routes import router as knowledge_router
|
||||
logger.info("knowledge_routes 导入成功")
|
||||
|
||||
# 注册路由
|
||||
self.app.include_router(webui_router)
|
||||
self.app.include_router(logs_router)
|
||||
self.app.include_router(knowledge_router)
|
||||
logger.info(f"knowledge_router 路由前缀: {knowledge_router.prefix}")
|
||||
|
||||
logger.info("✅ WebUI API 路由已注册")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 注册 WebUI API 路由失败: {e}")
|
||||
logger.error(f"❌ 注册 WebUI API 路由失败: {e}", exc_info=True)
|
||||
|
||||
async def start(self):
|
||||
"""启动服务器"""
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
from src.chat.knowledge.kg_manager import KGManager
|
||||
|
||||
kg = KGManager()
|
||||
kg.load_from_file()
|
||||
|
||||
edges = kg.graph.get_edge_list()
|
||||
if edges:
|
||||
e = edges[0]
|
||||
print(f"Edge tuple: {e}")
|
||||
print(f"Edge tuple type: {type(e)}")
|
||||
|
||||
edge_data = kg.graph[e[0], e[1]]
|
||||
print(f"\nEdge data type: {type(edge_data)}")
|
||||
print(f"Edge data: {edge_data}")
|
||||
print(f"Has 'get' method: {hasattr(edge_data, 'get')}")
|
||||
print(f"Is dict: {isinstance(edge_data, dict)}")
|
||||
|
||||
# 尝试不同的访问方式
|
||||
try:
|
||||
print(f"\nUsing []: {edge_data['weight']}")
|
||||
except Exception as e:
|
||||
print(f"Using [] failed: {e}")
|
||||
|
||||
try:
|
||||
print(f"Using .get(): {edge_data.get('weight')}")
|
||||
except Exception as e:
|
||||
print(f"Using .get() failed: {e}")
|
||||
|
||||
# 查看所有属性
|
||||
print(f"\nDir: {[x for x in dir(edge_data) if not x.startswith('_')]}")
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -5,13 +5,13 @@
|
|||
<link rel="icon" type="image/x-icon" href="/maimai.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MaiBot Dashboard</title>
|
||||
<script type="module" crossorigin src="/assets/index-DqR4PCua.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-Du48JcWB.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/react-vendor-Dtc2IqVY.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/router-BWgTyY51.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/charts-B1JvyJzO.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-nTGLnMlb.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/icons-D6w7t-x9.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B5LlQV5d.css">
|
||||
<link rel="modulepreload" crossorigin href="/assets/router-SinpzM5S.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/charts-BH1Uno6i.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-BLBhIcJ8.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/icons-COIni9ke.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Dq6na-LB.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue