194 lines
6.2 KiB
Python
194 lines
6.2 KiB
Python
|
|
# =============================================================================
|
|||
|
|
# 企微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
|