kgrag/kg_build_test.py
2026-07-29 18:10:19 +08:00

354 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import json
from typing import List, Tuple, Dict, Any
# --- 修复了拼写错误的导入路径 ---
from kg_build_v2.extract_wxanli_node_rela import get_wxanli_entity_rela
from kg_build_v2.extract_operation_node_rela import get_operation_node_rela
from kg_build_v2.extract_repair_node_rela import get_repair_node_rela
# 修正拼写extrat_normal -> extract_normal (请确保文件名实际为 extract_normal_node_rela.py)
from kg_build_v2.extrat_normal_node_rela import get_nromal_node_rela
from langchain_text_splitters import MarkdownHeaderTextSplitter, CharacterTextSplitter
from kg_build_v2.get_text_node_reala import extract_text_node_relationship
from kg_build_v2.utils import remove_duplicates, deduplicate_relationships
from modelsAPI.model_api import OpenaiAPI
import logging
# --- 日志配置 ---
logger = logging.getLogger(__name__)
if not logger.handlers:
handler = logging.StreamHandler()
fmt = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(fmt)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
def _split_markdown(text: str, chunk_size: int = 2048, chunk_overlap: int = 50) -> Tuple[List[str], List[str]]:
"""
将 Markdown 文本按一级标题分割,并对过长文本进行二次分块。
"""
headers_to_split_on = [("#", "Header_1")]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on, strip_headers=True)
chunks = splitter.split_text(text)
char_splitter = CharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
separator="\n\n"
)
final_chunks = []
start_chars = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "", "", "", "", "", "", "", "", "", "", "", "故障"}
# 提取有效标题
valid_headers = {
chunk.metadata.get('Header_1', '')
for chunk in chunks
if chunk.metadata.get('Header_1', '') and chunk.metadata['Header_1'][0] in start_chars
}
# 构建带标题前缀的块
for chunk in chunks:
header_1 = chunk.metadata.get('Header_1', '')
content = chunk.page_content
if len(content) <= 20000:
chunk_with_header = f"{header_1}\n{content}" if header_1 else content
final_chunks.append(chunk_with_header)
else:
split_texts = char_splitter.split_documents([chunk])
for split_chunk in split_texts:
chunk_with_header = f"{header_1}\n{split_chunk.page_content}" if header_1 else split_chunk.page_content
final_chunks.append(chunk_with_header)
# 重新分组合并
rechunked = []
rechunked_headers = []
current_content = ""
current_header = ""
for chunk in final_chunks:
# 检查是否为新章节
new_header = next((h for h in valid_headers if chunk.startswith(h + "\n")), None)
if new_header:
if current_content:
rechunked.append(current_content)
rechunked_headers.append(current_header)
current_content = chunk
current_header = new_header
else:
current_content += "\n" + chunk if current_content else chunk
# 添加最后一段
if current_content:
rechunked.append(current_content)
rechunked_headers.append(current_header)
return rechunked, rechunked_headers
def read_markdown_file(file_path: str) -> str:
"""
读取 Markdown 文件内容。
"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
except Exception as e:
logger.error(f"读取文件失败 [{file_path}]: {e}")
return ""
def get_html_node_rela(md_content: str) -> Tuple[List[Dict], List[Dict], str]:
"""
从 HTML/Markdown 表格中提取实体和关系。
"""
if not md_content:
return [], [], ""
all_entities = []
all_relations = []
cleaned_content = ""
# 1. 提取维修项目表
try:
repair_entities = get_repair_node_rela(md_content)
if isinstance(repair_entities, list):
all_entities.extend(repair_entities)
# --- 修复日志调用:使用 f-string ---
logger.info(f"维修项目表 - 实体数量: {len(repair_entities)}")
except Exception as e:
logger.error(f"提取维修项目表时发生错误: {e}")
# 2. 提取操作项目表
try:
result_operation = get_operation_node_rela(md_content)
operation_entities = []
operation_relations = []
if isinstance(result_operation, dict):
operation_entities = result_operation.get("entities", [])
operation_relations = result_operation.get("relationships", [])
elif isinstance(result_operation, list):
logger.warning("get_operation_node_rela 返回了列表而非字典")
all_entities.extend(operation_entities)
all_relations.extend(operation_relations)
# --- 修复日志调用 ---
logger.info(f"操作项目表 - 实体: {len(operation_entities)}, 关系: {len(operation_relations)}")
except Exception as e:
logger.error(f"提取操作项目表时发生错误: {e}")
# 3. 提取常规表格 (普通表、故障表等)
# --- 修复拼写错误get_nromal_node_rela -> get_nromal_node_rela ---
try:
result_normal, content_normal = get_nromal_node_rela(md_content)
normal_entities = []
normal_relations = []
if isinstance(result_normal, list) and result_normal:
item = result_normal[0]
if isinstance(item, dict):
normal_entities = item.get("entities", [])
normal_relations = item.get("relationships", [])
elif isinstance(result_normal, dict):
normal_entities = result_normal.get("entities", [])
normal_relations = result_normal.get("relationships", [])
all_entities.extend(normal_entities)
all_relations.extend(normal_relations)
cleaned_content = content_normal or ""
# --- 修复日志调用 ---
logger.info(f"常规表格 - 实体: {len(normal_entities)}, 关系: {len(normal_relations)}")
except Exception as e:
logger.error(f"提取常规表格时发生错误: {e}")
return all_entities, all_relations, cleaned_content
def get_wxanli_node_rela(md_content: str) -> Tuple[List[Dict], List[Dict]]:
"""
从维修案例中提取实体和关系。
"""
try:
result_wxanli = get_wxanli_entity_rela(md_content)
if isinstance(result_wxanli, dict):
wxanli_entities = result_wxanli.get("entities", [])
wxanli_relations = result_wxanli.get("relationships", [])
else:
wxanli_entities = []
wxanli_relations = []
logger.warning("维修案例返回数据格式异常,使用空列表代替")
except Exception as e:
logger.error(f"提取维修案例时发生错误: {e}")
wxanli_entities = []
wxanli_relations = []
return wxanli_entities, wxanli_relations
def build_repair_relationships(entities: List[Dict]) -> List[Dict]:
"""
根据维修项目编号和故障描述建立“修理”关系。
"""
faults = [e for e in entities if e.get("type") == "故障"]
maintenances = [e for e in entities if e.get("type") == "维修项目"]
relationships = []
for mt in maintenances:
mt_code = mt.get("properties", {}).get("维修项目编号")
mt_name = mt.get("properties", {}).get("名称")
if not mt_code or not mt_name:
continue
for fault in faults:
fault_name = fault.get("properties", {}).get("名称")
repair_text = fault.get("properties", {}).get("维修项目", "")
if not fault_name:
continue
if mt_code in repair_text:
relationships.append({
"type": "修理",
"from_entity": mt_name,
"to_entity": fault_name
})
return relationships
def get_md_node_relation(
md_content: str,
Ontology: List[str],
Ontology_DESCRIPTION: Dict[str, List[str]],
Relationships: List[str],
RELATIONSHIP_TYPES: Dict[str, List[str]],
NODE_TYPES: Dict[str, List[str]]
) -> Tuple[List[Dict], List[Dict]]:
"""
核心函数:协调各模块完成知识抽取、清洗、向量化。
"""
final_all_entity = []
final_all_relation = []
if not md_content:
logger.error("输入的 Markdown 内容为空")
return [], []
# 1. 处理表格类数据
table_entities, table_relations, cleaned_content = get_html_node_rela(md_content)
logger.info(f"表格类抽取 - 实体: {len(table_entities)}, 关系: {len(table_relations)}")
# 2. 处理维修案例
case_entities, case_relations = get_wxanli_node_rela(md_content)
logger.info(f"维修案例抽取 - 实体: {len(case_entities)}, 关系: {len(case_relations)}")
# 合并表格与案例
all_entities = table_entities + case_entities
all_relations = table_relations + case_relations
final_all_entity.extend(all_entities)
final_all_relation.extend(all_relations)
# 3. 建立修复关系
repair_relations = build_repair_relationships(all_entities)
final_all_relation.extend(repair_relations)
logger.info(f"构建故障修复关系数量: {len(repair_relations)}")
# 4. 处理纯文本
if cleaned_content.strip():
chunks, _ = _split_markdown(cleaned_content)
text_entities, text_relationships = extract_text_node_relationship(
chunks,
ontology=Ontology,
Ontology_DESCRIPTION=Ontology_DESCRIPTION,
relationships=Relationships,
relationship_types=RELATIONSHIP_TYPES,
node_types=NODE_TYPES
)
final_all_entity.extend(text_entities)
final_all_relation.extend(text_relationships)
logger.info(f"文本抽取 - 实体: {len(text_entities)}, 关系: {len(text_relationships)}")
else:
logger.info("未检测到有效清洗文本,跳过文本抽取步骤")
# 5. 去重
final_all_entity = remove_duplicates(final_all_entity)
final_all_relation = deduplicate_relationships(final_all_relation)
logger.info(f"去重后 - 总实体: {len(final_all_entity)}, 总关系: {len(final_all_relation)}")
# 6. 实体向量化
# for entity in final_all_entity:
# name = entity["properties"].get("名称")
# if name:
# --- 优化变量名 ---
# embedding_vector = OpenaiAPI.generate_embedding(name)
# entity["properties"]["embedding"] = embedding_vector
# logger.info("实体向量化完成")
return final_all_entity, final_all_relation
if __name__ == "__main__":
input_directory = "/app/测试FDJwx手册_shenghna_output11_full_content.md"
final_all_entity = []
final_all_relation = []
# 调用目录处理函数
mdcontent = read_markdown_file(input_directory)
if mdcontent is None:
print("无法读取文件,程序退出")
exit(1)
Ontology = [
"舰艇", "系统", "设备", "零部件", "故障", "修理方案", "维修项目", "操作项目", "保养", "备品备件"
]
Ontology_DESC = {
"舰艇": ["代表一艘完整海上作战或保障平台的实体,是所有系统、设备和零部件的最高层级载体。区别于“系统”或“设备”,舰艇具有独立的型号、弦号和部队归属,不可作为其他舰艇的组成部分。"],
"系统": ["舰艇中实现某一类功能(如动力、导航、通信)的集成化功能单元,由多个设备协同组成。与“设备”不同,系统本身不直接对应物理装置,而是逻辑/功能层面的集合;与“舰艇”不同,系统不具备独立航行能力或部队归属。"],
"设备": ["安装在舰艇上、具有独立型号和运行状态的物理装置,可被启停、监控运行时间,并可能产生故障。区别于“零部件”,设备是可独立更换或维修的功能单元;区别于“系统”,设备是具体硬件而非功能集合。"],
"零部件": ["构成设备的不可再拆分的基础物理元件(如轴承、电路板、密封圈),通常无独立运行状态或启用时间。与“设备”关键区别在于:零部件不能单独运行,也不记录运行时间或技术状态变更。"],
"故障": ["设备或系统在运行中出现的异常状态或失效事件,必须包含具体现象(如“无法启动”)、原因(如“电源短路”)及影响。区别于“修理”或“保养”,故障是问题本身,而非应对措施;不包含操作步骤或人员信息。"],
"修理方案": ["针对已发生故障所执行的一次性修复行为,包含具体时间、人员、工具、方案和所关联的维修项目。区别于“保养”,修理是响应式、非周期性的;区别于“维修项目”,修理是实际发生的实例,而非标准化规程。"],
"维修项目": ["标准化、可重复执行的维修任务规程,规定了某类设备或系统在特定条件下应如何维修,包含编号、步骤、材料、安全要求等。区别于“修理”,维修项目是模板或规范,而非具体事件;不包含实际发生时间或人员。"],
"操作项目": ["舰艇正常运行中需执行的标准操作流程(如启动主机、校准雷达),强调操作步骤与安全保障。区别于“维修项目”,操作使用项目用于日常使用而非维护"],
"保养" : ["按计划周期性执行的预防性维护活动(如润滑、清洁、检测),目的是延缓性能退化。"]
}
relationships = [
"包含", "组成", "解决", "用于", "使用", "具有","存储备用"
]
relationship_types = {
"包含": ["舰艇包含系统", "系统包含设备", "设备包含零部件", "修理包含维修项目"],
"解决": ["修理解决故障", "维修项目解决故障"],
"使用": ["设备使用操作项目", "设备使用保养", "设备使用维修项目"],
"具有": ["设备具有故障", "零部件具有故障"],
"存储备用" : ["设备存储备用备品备件"]
}
node_types = {
"舰艇": ["名称", "型号", "弦号", "设计单位", "总装工厂", "所属部队"],
"系统": ["名称", "技术责任单位", "生产单位", "级别"],
"设备": ["名称", "型号", "技术责任单位", "生产单位", "技术状态", "生产日期", "启用时间", "运行时间", "运行条件"],
"零部件": ["名称", "型号", "技术责任单位", "生产单位"],
"故障": ["名称", "故障原因", "故障影响", "故障类型", "发生时间", "处置情况", "修复情况"],
"修理方案": ["名称", "修理人员", "修理时间", "修理工具", "备品备件", "修理类型", "维修项目"],
"维修项目": ["名称", "维修项目编号", "组成编码", "维修设备名称", "维修间隔期", "维修级别", "页码", "安全防护要求", "安全注意事项", "工具", "备品备件", "消耗材料", "人力人员", "修理步骤"],
"操作项目": ["名称", "操作项目编号", "安全防护", "注意事项", "保障设施、保障设备(工具)", "人力人员", "操作步骤"],
"保养": ["保养人员", "保养时间", "保养工具", "保养频率"],
"备品备件" : ["名称", "规格", "型号", "图号或标准号或代号","计量单位", "单机重量", "尺寸长X宽X高mm", "单机装配数量","实物图片电子文档名称","MTBF(b)","生成厂家",
"出厂日期",
"供货周期(天)",
"有效期(月)",
"使用寿命h)",
"备件箱号",
"储存要求_环境",
"储存要求_期限",
"停产后供应方式"],
}
final_all_entity,final_all_relation = get_md_node_relation(mdcontent,Ontology=Ontology,Ontology_DESCRIPTION=Ontology_DESC,Relationships=relationships,RELATIONSHIP_TYPES=relationship_types,NODE_TYPES=node_types)