import json import os import re from dotenv import load_dotenv from neo4j import AsyncGraphDatabase from typing import Any, Dict, List, Optional load_dotenv() NEO4J_CONFIG = { "uri": os.getenv("NEO4J_URI", "bolt://192.168.0.46:57687"), "user": os.getenv("NEO4J_USER", "neo4j"), "password": os.getenv("NEO4J_PASSWORD","zdht123@"), } def _string_similarity(a: str, b: str) -> float: """ 简单字符串相似度:综合长度差、包含关系和公共字符比例。 0~1 之间,越大越相似。 """ if not a or not b: return 0.0 a = str(a).strip().lower() b = str(b).strip().lower() if not a or not b: return 0.0 if a == b: return 1.0 if a in b or b in a: # 明确包含关系,给较高分 return 0.9 # 公共字符集比例 set_a, set_b = set(a), set(b) inter = len(set_a & set_b) union = len(set_a | set_b) or 1 jaccard = inter / union # 长度差惩罚 len_diff = abs(len(a) - len(b)) len_penalty = max(0.0, 1.0 - len_diff / max(len(a), len(b), 1)) return 0.5 * jaccard + 0.5 * len_penalty def _pick_best_by_name(candidates: List[Dict[str, Any]], target_name: str) -> Optional[Dict[str, Any]]: """ 在候选节点中,根据名称相似度选择最优节点。 约定每个节点为:{id, labels, props, display_name} """ if not candidates or not target_name: return None best = None best_score = 0.0 for item in candidates: name = (item.get("display_name") or "").strip() if not name: # 尝试从属性中推断名称 props = item.get("props") or {} name = props.get("name") or props.get("名称") or props.get("设备名称") or "" score = _string_similarity(target_name, name) if score > best_score: best_score = score best = item # 设置一个最低阈值,过于不相似则认为未命中 return best if best_score >= 0.3 else None def _extract_mt_codes(text: str) -> List[str]: """ 从文本中抽取形如 MT007 的维修项目编号(MT + 3位数字)。 """ if not text: return [] codes = re.findall(r"\bMT\d{3}\b", str(text)) # 去重但保持顺序 seen = set() ordered: List[str] = [] for c in codes: if c not in seen: seen.add(c) ordered.append(c) return ordered def _node_record_to_dict(record: Any) -> Dict[str, Any]: """ 将 Cypher 返回的 id/labels/props 结构统一为字典。 约定 Cypher 中使用: RETURN id(n) AS id, labels(n) AS labels, properties(n) AS props, <可选的 display_name> """ # neo4j.Record 提供 data() 方法;若不是 Record,则假定本身为 dict if hasattr(record, "data"): data = record.data() else: data = record or {} return { "id": data.get("id"), "labels": data.get("labels") or [], "props": data.get("props") or {}, "display_name": data.get("display_name") if "display_name" in data else None, } def _to_graph_display_node(node_dict: Dict[str, Any]) -> Dict[str, Any]: """将内部节点格式转为 graph_search 展示用节点:id、name、label。""" nid = node_dict.get("id") props = node_dict.get("props") or {} name = ( node_dict.get("display_name") or props.get("名称") or props.get("name") or str(nid) ) labels = node_dict.get("labels") or [] label = labels[0] if labels else "" return {"id": nid, "name": name, "label": label} def _build_fault_graph_display( ship_node: Optional[Dict[str, Any]], device_node: Optional[Dict[str, Any]], fault_mode_node: Optional[Dict[str, Any]], repair_projects: List[Dict[str, Any]], ) -> tuple: """ 构建与 graph_search 一致的图谱展示结构:nodes 与 links。 返回 (nodes: list, links: list)。 关系:舰艇-包含->设备,设备-存在->故障模式,设备-维修时使用->维修项目。 """ nodes: List[Dict[str, Any]] = [] links: List[Dict[str, Any]] = [] seen_ids = set() def add_node(nd: Optional[Dict[str, Any]]): if not nd or nd.get("id") in seen_ids: return seen_ids.add(nd["id"]) nodes.append(_to_graph_display_node(nd)) def add_link(source_id: Any, target_id: Any, rel_type: str = ""): links.append({"source": source_id, "target": target_id, "type": rel_type}) if ship_node: add_node(ship_node) if device_node: add_node(device_node) if ship_node: add_link(ship_node["id"], device_node["id"], "包含") if fault_mode_node: add_node(fault_mode_node) if device_node: add_link(device_node["id"], fault_mode_node["id"], "存在") for p in repair_projects or []: add_node(p) if device_node: add_link(device_node["id"], p["id"], "维修时使用") return nodes, links def _build_fault_graph_results_text( ship_node: Optional[Dict[str, Any]], device_node: Optional[Dict[str, Any]], fault_mode_node: Optional[Dict[str, Any]], fault_reason: str, repair_plan: str, mt_codes: List[str], repair_projects: List[Dict[str, Any]], ship_number: str, device_name: str, fault_symptom: str, ) -> str: """生成与 graph_rag_search 类似的一段话,供展示与下游使用。""" ship_name = "" if ship_node: ship_name = (ship_node.get("display_name") or (ship_node.get("props") or {}).get("名称") or "").strip() device_name_d = "" if device_node: device_name_d = (device_node.get("display_name") or (device_node.get("props") or {}).get("名称") or "").strip() fault_name = "" if fault_mode_node: fault_name = (fault_mode_node.get("display_name") or (fault_mode_node.get("props") or {}).get("名称") or "").strip() parts = [ f"根据舷号「{ship_number}」、设备「{device_name}」、故障现象「{fault_symptom}」进行图谱检索:", f"定位到舰艇「{ship_name or '未知'}」、设备节点「{device_name_d or '未知'}」、故障模式「{fault_name or '未知'}」。", ] if fault_reason: parts.append(f"故障原因:{fault_reason}") if repair_plan: parts.append(f"维修方案摘要:{repair_plan[:500]}{'…' if len(repair_plan) > 500 else ''}") if mt_codes: parts.append(f"涉及维修项目编号:{'、'.join(mt_codes)}。") if repair_projects: proj_names = [] for p in repair_projects: nm = (p.get("display_name") or (p.get("props") or {}).get("名称") or (p.get("props") or {}).get("维修项目编号") or "").strip() if nm: proj_names.append(nm) if proj_names: parts.append(f"匹配到的维修项目:{'、'.join(proj_names)}。") return "\n".join(parts) async def fault_graph_reason_and_projects( ship_number: str, device_name: str, fault_symptom: str, ) -> Dict[str, Any]: """ 图谱检索工具 """ if not ship_number or not str(ship_number).strip(): return {"success": False, "error": "舷号不能为空"} if not device_name or not str(device_name).strip(): return {"success": False, "error": "设备名称不能为空"} if not fault_symptom or not str(fault_symptom).strip(): return {"success": False, "error": "故障现象不能为空"} uri = NEO4J_CONFIG.get("uri") user = NEO4J_CONFIG.get("user") password = NEO4J_CONFIG.get("password") if not uri or not user or not password: return {"success": False, "error": "Neo4j 配置不完整,请检查 NEO4J_CONFIG"} try: async with AsyncGraphDatabase.driver(uri, auth=(user, password)) as driver: async with driver.session() as session: ship_key = str(ship_number).strip() device_key = str(device_name).strip() # 1) 先按舷号定位舰艇节点(限定范围) ship_query = """ MATCH (s:舰艇) WHERE s.舷号 IN [$ship_key] RETURN DISTINCT id(s) AS id, labels(s) AS labels, properties(s) AS props, coalesce(s.名称, s.name, s.舷号) AS display_name LIMIT 10 """ ship_records = await session.run(ship_query, ship_key=ship_key) ship_candidates = [_node_record_to_dict(r) async for r in ship_records] if not ship_candidates: return {"success": False, "error": "在图谱中未找到对应舷号的舰艇节点"} ship_node = ship_candidates[0] ship_id = ship_node["id"] # 2) 在该舰艇范围内定位设备候选节点 device_query = """ MATCH (ship:舰艇) WHERE id(ship) = $ship_id MATCH (ship)-[:包含*0..8]->(n:设备) WITH n, coalesce(n.名称, n.name, n.设备名称) AS nm WHERE nm IS NOT NULL AND ( toLower(nm) CONTAINS toLower($device_key) OR toLower($device_key) CONTAINS toLower(nm) ) RETURN DISTINCT id(n) AS id, labels(n) AS labels, properties(n) AS props, nm AS display_name LIMIT 50 """ dev_records = await session.run( device_query, ship_id=ship_id, device_key=device_key ) device_candidates = [_node_record_to_dict(r) async for r in dev_records] device_node = _pick_best_by_name(device_candidates, device_key) if not device_node: return {"success": False, "error": "在该舰艇范围内未找到匹配的设备节点"} device_id = device_node["id"] # 3) 设备 -> (包含层级中的真实设备)-> 维修工作 -> 故障模式 # 路径示例: # (start_e:设备 {名称:'主发动机'})-[:包含*0..8]-(real_e:设备) # -[:发生故障使用维修工作]-(w:维修工作)-[:包含故障]-(fm:故障模式) fault_mode_query = """ MATCH (start_e:设备) WHERE id(start_e) = $device_id MATCH (start_e)-[:包含*0..8]-(real_e:设备) MATCH (real_e)-[:发生故障使用维修工作]-(w:维修工作)-[:包含故障]-(fm:故障模式) RETURN DISTINCT id(fm) AS id, labels(fm) AS labels, properties(fm) AS props, fm.名称 AS display_name """ fm_records = await session.run(fault_mode_query, device_id=device_id) fault_modes = [_node_record_to_dict(r) async for r in fm_records] if not fault_modes: return {"success": False, "error": "在该设备节点下未找到故障模式节点"} fault_mode_node = _pick_best_by_name(fault_modes, fault_symptom) if not fault_mode_node: return {"success": False, "error": "未能在故障模式节点中找到与故障现象足够匹配的节点"} fault_mode_id = fault_mode_node.get("id") if fault_mode_id is None: return {"success": False, "error": "故障模式节点缺少 id,无法继续检索维修项目"} fm_props = fault_mode_node.get("props") or {} fault_reason = fm_props.get("故障原因") or "" repair_plan = fm_props.get("维修方案") or "" mt_codes = _extract_mt_codes(repair_plan) # 4) 故障模式 -> 维修项目(维修方案) repair_project_query = """ MATCH (fm:故障模式) WHERE id(fm) = $fault_mode_id OPTIONAL MATCH (fm)<-[:修理]-(p:维修项目) RETURN DISTINCT id(p) AS id, labels(p) AS labels, properties(p) AS props, p.名称 AS display_name """ rp_records = await session.run( repair_project_query, fault_mode_id=fault_mode_id ) all_projects = [_node_record_to_dict(r) async for r in rp_records] repair_projects: List[Dict[str, Any]] = [] if mt_codes: for proj in all_projects: props = proj.get("props") or {} proj_code = (props.get("维修项目编号") or "").strip() if proj_code and ( proj_code in mt_codes or any(c in proj_code for c in mt_codes) ): repair_projects.append(proj) elif not proj_code: values_text = " ".join( [str(v) for v in props.values() if v is not None] ) if any(code in values_text for code in mt_codes): repair_projects.append(proj) else: repair_projects = all_projects repair_ids = [ p.get("id") for p in repair_projects if p.get("id") is not None ] exclude_keys = { "last_updated", "created_at", "knowledge_source", "fulltext", "embedding", } # 4) nodes:包含路径上的设备 / 维修工作 / 故障模式 / 维修项目 graph_nodes_query = """ MATCH (ship:舰艇) WHERE id(ship) = $ship_id MATCH (start_e:设备) WHERE id(start_e) = $device_id MATCH path0 = (ship)-[:包含*0..8]->(start_e) MATCH path1 = (start_e)-[:包含*0..8]-(real_e:设备) MATCH path2 = (real_e)-[:发生故障使用维修工作]-(w:维修工作)-[:包含故障]-(fm:故障模式) WHERE id(fm) = $fault_mode_id OPTIONAL MATCH (fm)<-[:修理]-(rp:维修项目) WHERE id(rp) IN $repair_ids WITH path0, path1, path2, collect(DISTINCT rp) AS rps WITH nodes(path0) + nodes(path1) + nodes(path2) + rps AS ns UNWIND ns AS n WITH DISTINCT n WHERE n IS NOT NULL RETURN elementId(n) AS id, labels(n) AS labels, coalesce(n.名称, n.name) AS name, properties(n) AS props """ node_records = await session.run( graph_nodes_query, ship_id=ship_id, device_id=device_id, fault_mode_id=fault_mode_id, repair_ids=repair_ids or [-1], ) nodes_g: List[Dict[str, Any]] = [] async for r in node_records: data_r = r.data() nid = data_r.get("id") if nid is None: continue labels = data_r.get("labels") or [] name = data_r.get("name") or str(nid) props = data_r.get("props") or {} props_filtered = { k: v for k, v in props.items() if k not in exclude_keys } nodes_g.append( { "id": nid, "name": name, "labels": labels, "properties": props_filtered, } ) # 5) links:包含 / 发生故障使用维修工作 / 包含故障 / 修理 graph_links_query = """ MATCH (ship:舰艇) WHERE id(ship) = $ship_id MATCH (start_e:设备) WHERE id(start_e) = $device_id MATCH path0 = (ship)-[:包含*0..8]->(start_e) MATCH path1 = (start_e)-[:包含*0..8]-(real_e:设备) MATCH path2 = (real_e)-[:发生故障使用维修工作]-(w:维修工作)-[:包含故障]-(fm:故障模式) WHERE id(fm) = $fault_mode_id WITH relationships(path0) + relationships(path1) + relationships(path2) AS rs UNWIND rs AS r RETURN elementId(r) AS id, type(r) AS label, elementId(startNode(r)) AS source, elementId(endNode(r)) AS target, properties(r) AS props UNION MATCH (fm:故障模式) WHERE id(fm) = $fault_mode_id MATCH (fm)<-[r:修理]-(rp:维修项目) WHERE id(rp) IN $repair_ids RETURN elementId(r) AS id, type(r) AS label, elementId(startNode(r)) AS source, elementId(endNode(r)) AS target, properties(r) AS props """ link_records = await session.run( graph_links_query, ship_id=ship_id, device_id=device_id, fault_mode_id=fault_mode_id, repair_ids=repair_ids or [-1], ) links_g: List[Dict[str, Any]] = [] async for r in link_records: data_r = r.data() src = data_r.get("source") tgt = data_r.get("target") if src is None or tgt is None: continue rel_props = data_r.get("props") or {} rel_props_filtered = { k: v for k, v in rel_props.items() if k not in exclude_keys } links_g.append( { "id": data_r.get("id"), "label": data_r.get("label", ""), "source": src, "target": tgt, "properties": rel_props_filtered, } ) node_ids = {n["id"] for n in nodes_g} links_g = [ l for l in links_g if l.get("source") in node_ids and l.get("target") in node_ids ] repair_plans_data: List[Dict[str, Any]] = [] for p in repair_projects: props = p.get("props") or {} filtered = {k: v for k, v in props.items() if k not in exclude_keys} if filtered: repair_plans_data.append(filtered) results_obj = {"fault_reason": fault_reason, "repair_plans": repair_plans_data} results_text = json.dumps(results_obj, ensure_ascii=False) graph_item = {"nodes": nodes_g, "links": links_g, "results": results_text} return {"success": True, "xxxx.graph": [graph_item]} except Exception as e: return {"success": False, "error": f"Neo4j 查询失败: {str(e)}"}