Files
wecom_it_smart_desk/backend/app/services/graph_query_service.py
T

194 lines
6.2 KiB
Python
Raw Normal View History

# =============================================================================
# 企微IT智能服务台 — 图谱查询服务
# =============================================================================
# 说明:基于Neo4j知识图谱的智能问答服务。
# 用户问题 → 图谱查询 → 直接返回解决方案(毫秒级响应)
# 未命中 → 降级到Dify流程
#
# 核心功能:
# 1. 关键词提取(jieba分词 + 停用词过滤)
# 2. Neo4j模糊匹配Issue节点
# 3. 查找关联的Action解决方案
# 4. 返回最高匹配的SolutionResult
# =============================================================================
import logging
from typing import List, Optional
from pydantic import BaseModel
logger = logging.getLogger(__name__)
# 停用词列表(常见的无意义词汇)
STOPWORDS = {
"", "", "", "", "", "", "", "", "", "",
"", "", "一个", "", "", "", "", "", "", "",
"", "", "", "没有", "", "", "自己", "", "",
"什么", "怎么", "如何", "为什么", "请问", "帮忙", "帮助",
}
class SolutionResult(BaseModel):
"""图谱查询结果 - 解决方案"""
issue_name: str = ""
issue_uuid: str = ""
solution: str = ""
action_name: str = ""
confidence: float = 0.0
class GraphQueryService:
"""图谱查询服务 - 根据用户问题查找解决方案"""
def __init__(self, neo4j_client):
"""初始化图谱查询服务
Args:
neo4j_client: Neo4jClient实例
"""
self.neo4j_client = neo4j_client
self._keyword_cache: dict = {} # 简单缓存
async def find_solution_by_question(
self, question: str, timeout_ms: int = 100
) -> Optional[SolutionResult]:
"""根据用户问题查找解决方案(图谱查询主入口)
流程:
1. 提取问题关键词
2. 遍历关键词查询Neo4j图谱
3. 获取匹配的Issue及关联的Action
4. 返回置信度最高的解决方案
Args:
question: 用户问题文本
timeout_ms: 查询超时时间(毫秒),默认100ms
Returns:
Optional[SolutionResult]: 匹配的解决方案,未命中返回None
"""
if not question or not question.strip():
return None
# 1. 提取关键词
keywords = self._extract_keywords(question)
if not keywords:
logger.info(f"图谱查询:无法提取关键词 question={question[:50]}")
return None
logger.info(f"图谱查询:keywords={keywords}, question={question[:50]}")
# 2. 遍历关键词查询图谱(按置信度排序)
for kw in keywords:
try:
result = await self._query_by_keyword(kw)
if result:
logger.info(
f"图谱命中: keyword={kw}, issue={result.issue_name}, "
f"confidence={result.confidence}"
)
return result
except Exception as e:
logger.warning(f"图谱查询异常(降级继续): keyword={kw}, error={e}")
continue
logger.info(f"图谱未命中: question={question[:50]}")
return None
def _extract_keywords(self, text: str) -> List[str]:
"""从文本中提取关键词
使用jieba分词 + 停用词过滤
Args:
text: 输入文本
Returns:
List[str]: 关键词列表(按出现顺序)
"""
# 尝试导入jieba,如果失败则使用简单分词
try:
import jieba
words = jieba.lcut(text)
except ImportError:
# 降级:简单按空格和标点分词
words = text.replace("", " ").replace("", " ").replace("", " ").split()
# 过滤停用词和短词
keywords = [
w.strip()
for w in words
if w.strip() and len(w.strip()) >= 2 and w.strip() not in STOPWORDS
]
# 返回前5个关键词(避免查询过多)
return keywords[:5]
async def _query_by_keyword(self, keyword: str) -> Optional[SolutionResult]:
"""根据单个关键词查询解决方案
Args:
keyword: 搜索关键词
Returns:
Optional[SolutionResult]: 匹配的解决方案
"""
# 调用neo4j_client的模糊搜索方法
if not hasattr(self.neo4j_client, "find_issues_by_keyword"):
logger.warning("neo4j_client没有find_issues_by_keyword方法")
return None
issues = await self.neo4j_client.find_issues_by_keyword(keyword, limit=5)
if not issues:
return None
# 获取第一个Issue的详细信息和关联的Action
issue = issues[0]
# 查询关联的Action解决方案
actions = await self.neo4j_client.find_actions_by_issue(issue.uuid)
if not actions:
return None
action = actions[0]
# 构建返回结果
return SolutionResult(
issue_name=issue.name,
issue_uuid=issue.uuid,
solution=action.description or action.name,
action_name=action.name,
confidence=0.8, # 简化处理,默认0.8
)
# 全局单例
_graph_query_service: Optional[GraphQueryService] = None
async def get_graph_query_service(neo4j_client=None) -> Optional[GraphQueryService]:
"""获取GraphQueryService单例(异步版本)
Args:
neo4j_client: Neo4jClient实例,不传则自动获取
Returns:
Optional[GraphQueryService]: 图谱查询服务实例,Neo4j不可用时返回None
"""
global _graph_query_service
if _graph_query_service is None:
if neo4j_client is None:
from app.services.neo4j_client import get_neo4j_client
neo4j_client = await get_neo4j_client()
# 如果neo4j_client为None(图服务不可用),直接返回None
if neo4j_client is None:
return None
_graph_query_service = GraphQueryService(neo4j_client)
return _graph_query_service