data_check_tools.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. import json
  2. from typing import Annotated, Any
  3. from datetime import datetime
  4. import logging
  5. from langchain.tools import tool, InjectedState
  6. from service.data_service import VentDataService
  7. # 日志配置
  8. logging.basicConfig(level=logging.INFO, format="【%(name)s】%(message)s")
  9. logger = logging.getLogger("FormReviewTool")
  10. def _extract_mine_name(ventilation_plan: dict) -> str:
  11. """Extract the mine name string from a ventilation plan dict."""
  12. return ventilation_plan.get("mine_info", {}).get("mine_name", "")
  13. def map_temperature_to_velocity_range(temp_c: float) -> str:
  14. """根据进风流温度,返回对应的采面风速范围字符串"""
  15. if temp_c < 20:
  16. return "1.0"
  17. elif 20 <= temp_c < 23:
  18. return "1.0 ~ 1.5"
  19. elif 23 <= temp_c < 26:
  20. return "1.5 ~ 1.8"
  21. elif 26 <= temp_c < 28:
  22. return "1.8 ~ 2.5"
  23. elif 28 <= temp_c <= 30:
  24. return "2.5 ~ 3.0"
  25. else:
  26. return "超出范围"
  27. @tool
  28. def form_review(
  29. state: Annotated[dict, InjectedState]
  30. ) -> str:
  31. """
  32. 配风计划形式审查综合工具。
  33. 检查五项合规性:
  34. 1. 四级签字(编制人、通风科长、通风副总、总工程师)是否齐全
  35. 2. 编制时间是否早于配风月份(即是否为上月编制)
  36. 3. 所有用风地点是否具备风量计算与核验过程
  37. 4. 是否列出全部用风地点
  38. 5. 多回风井场景下是否按区域分别计算需风量
  39. """
  40. # 从注入状态获取配风数据
  41. ventilation_plan = state.get("vent_plan_data", {})
  42. if not ventilation_plan:
  43. return "❌ 错误:未读取到配风计划数据"
  44. logger.info("===== 开始执行形式审查 =====")
  45. audit_results: list[str] = []
  46. passed_count = 0
  47. failed_count = 0
  48. mine_basic_info = ventilation_plan.get("mine_info", {})
  49. coal_faces = ventilation_plan.get("coal_faces", [])
  50. tunneling_faces = ventilation_plan.get("tunneling_faces", [])
  51. chambers = ventilation_plan.get("chambers", [])
  52. other_points = ventilation_plan.get("other_points", [])
  53. # 检查1:四级审核签字
  54. logger.info("执行检查:1/5 审核签字校验")
  55. try:
  56. required_signatures: list[tuple[str, str]] = [
  57. ("reviewer", "编制人"),
  58. ("ventilation_chief", "通风科长"),
  59. ("ventilation_deputy", "通风副总"),
  60. ("chief_engineer", "总工程师")
  61. ]
  62. missing_signatures: list[str] = []
  63. for field, name in required_signatures:
  64. val = mine_basic_info.get(field, "")
  65. if val is None or str(val).strip() == "":
  66. missing_signatures.append(name)
  67. if missing_signatures:
  68. result_line = f"❌ 签字审查不通过:缺失 {','.join(missing_signatures)}"
  69. audit_results.append(result_line)
  70. failed_count += 1
  71. logger.warning(result_line)
  72. else:
  73. result_line = "✅ 签字审查通过:四级签字齐全"
  74. audit_results.append(result_line)
  75. passed_count += 1
  76. logger.info(result_line)
  77. except Exception as e:
  78. result_line = f"❌ 签字审查异常:{str(e)}"
  79. audit_results.append(result_line)
  80. failed_count += 1
  81. logger.error(result_line)
  82. # 检查2:编制时间是否为上月
  83. logger.info("执行检查:2/5 编制时间合规性校验")
  84. try:
  85. plan_month = mine_basic_info.get("vent_date", "")
  86. preparation_date = mine_basic_info.get("preparation_date", "")
  87. logger.info(f"→ 配风月份:{plan_month}")
  88. logger.info(f"→ 编制日期:{preparation_date}")
  89. if not plan_month or not preparation_date:
  90. result_line = "❌ 时间审查不通过:日期字段为空"
  91. audit_results.append(result_line)
  92. failed_count += 1
  93. logger.warning(result_line)
  94. else:
  95. py, pm = map(int, plan_month.split("-"))
  96. prep_dt = datetime.strptime(preparation_date[:10], "%Y-%m-%d")
  97. is_timely = (prep_dt.year < py) or (prep_dt.year == py and prep_dt.month < pm)
  98. if is_timely:
  99. result_line = "✅ 时间审查通过:编制时间符合上月要求"
  100. audit_results.append(result_line)
  101. passed_count += 1
  102. logger.info(result_line)
  103. else:
  104. result_line = "❌ 时间审查不通过:编制时间非上月"
  105. audit_results.append(result_line)
  106. failed_count += 1
  107. logger.warning(result_line)
  108. except Exception as e:
  109. result_line = f"❌ 时间审查异常:{str(e)}"
  110. audit_results.append(result_line)
  111. failed_count += 1
  112. logger.error(result_line)
  113. # 检查3:风量计算核验过程
  114. logger.info("执行检查:3/5 风量计算核验过程完整性校验")
  115. try:
  116. locations_without_calc: list[str] = []
  117. for item in coal_faces:
  118. if not item.get("has_calc_check_process"):
  119. locations_without_calc.append(f"回采工作面「{item.get('face_name', '')}」")
  120. for item in tunneling_faces:
  121. if not item.get("has_calc_check_process"):
  122. locations_without_calc.append(f"掘进工作面「{item.get('face_name', '')}」")
  123. for item in chambers:
  124. if not item.get("has_calc_check_process"):
  125. locations_without_calc.append(f"硐室「{item.get('name', '')}」")
  126. for item in other_points:
  127. if not item.get("has_calc_check_process"):
  128. locations_without_calc.append(f"其他地点「{item.get('name', '')}」")
  129. logger.info(f"→ 缺失计算过程地点数量:{len(locations_without_calc)}")
  130. if locations_without_calc:
  131. lack_str = ";".join(locations_without_calc)
  132. result_line = f"❌ 计算过程不通过:以下 {len(locations_without_calc)} 个地点未提供风量计算与核验过程:{lack_str}"
  133. audit_results.append(result_line)
  134. failed_count += 1
  135. logger.warning(result_line)
  136. else:
  137. result_line = "✅ 计算过程通过:全部地点均具备完整风量计算与核验过程"
  138. audit_results.append(result_line)
  139. passed_count += 1
  140. logger.info(result_line)
  141. except Exception as e:
  142. result_line = f"❌ 计算过程检查异常:{str(e)}"
  143. audit_results.append(result_line)
  144. failed_count += 1
  145. logger.error(result_line)
  146. # 检查4:是否列出全部用风地点
  147. logger.info("执行检查:4/5 用风地点清单完整性校验")
  148. try:
  149. lists_all_locations = mine_basic_info.get("list_all_vent_place", False)
  150. logger.info(f"→ list_all_vent_place = {lists_all_locations}")
  151. if lists_all_locations:
  152. result_line = "✅ 用风地点通过:已全部列出"
  153. audit_results.append(result_line)
  154. passed_count += 1
  155. logger.info(result_line)
  156. else:
  157. result_line = "❌ 用风地点不通过:未全部列出"
  158. audit_results.append(result_line)
  159. failed_count += 1
  160. logger.warning(result_line)
  161. except Exception as e:
  162. result_line = f"❌ 用风地点异常:{str(e)}"
  163. audit_results.append(result_line)
  164. failed_count += 1
  165. logger.error(result_line)
  166. # 检查5:多回风井分区计算
  167. logger.info("执行检查:5/5 多回风井分区计算校验")
  168. try:
  169. return_well_count = mine_basic_info.get("return_well_count", 1)
  170. has_zone_calculation = mine_basic_info.get("return_well_zone_calc", False)
  171. logger.info(f"→ 回风井数量:{return_well_count}")
  172. logger.info(f"→ 是否分区计算:{has_zone_calculation}")
  173. if return_well_count >= 2:
  174. if has_zone_calculation:
  175. result_line = "✅ 回风井分区通过:已按区域计算"
  176. audit_results.append(result_line)
  177. passed_count += 1
  178. logger.info(result_line)
  179. else:
  180. result_line = "❌ 回风井分区不通过:多回风井未分区计算"
  181. audit_results.append(result_line)
  182. failed_count += 1
  183. logger.warning(result_line)
  184. else:
  185. result_line = "✅ 回风井分区无需校验:单回风井"
  186. audit_results.append(result_line)
  187. passed_count += 1
  188. logger.info(result_line)
  189. except Exception as e:
  190. result_line = f"❌ 回风井分区异常:{str(e)}"
  191. audit_results.append(result_line)
  192. failed_count += 1
  193. logger.error(result_line)
  194. # 汇总输出
  195. final_status = "✅ 形式审查全部通过" if failed_count == 0 else "❌ 形式审查存在不合格项"
  196. output = "".join(audit_results)
  197. logger.info(f"===== 形式审查完成 =====")
  198. logger.info(f"通过:{passed_count} 项 | 不通过:{failed_count} 项 | 结果:{final_status}")
  199. return f"""
  200. 【形式审查汇总结果】
  201. {output}
  202. --------------------------------
  203. 最终结论:{final_status}
  204. 通过项:{passed_count} 项
  205. 不通过项:{failed_count}
  206. """
  207. @tool
  208. def check_current_month_plan(
  209. state: Annotated[dict, InjectedState]
  210. ) -> str:
  211. """
  212. 验证配风计划月份是否为当月最新版本。
  213. 规则:将配风计划中的 vent_date 字段与当前系统年月比对,
  214. 一致则判定为最新版,否则提示版本过期。
  215. """
  216. ventilation_plan = state.get("vent_plan_data", {})
  217. mine_basic_info = ventilation_plan.get("mine_info", {})
  218. plan_month = mine_basic_info.get("vent_date", "")
  219. logger.info("开始检查配风计划是否为当月最新版本 (verify_month_version)")
  220. current_system_month = datetime.now().strftime("%Y-%m")
  221. logger.info(f"当前系统月份:{current_system_month}")
  222. logger.info(f"配风计划月份:{plan_month}")
  223. if not plan_month:
  224. logger.error("检查失败:plan_month 字段为空,无法校验版本")
  225. return "检查失败:配风计划月份不能为空"
  226. if len(plan_month) != 7 or plan_month[4] != "-":
  227. logger.error(f"检查失败:日期格式错误,必须为 YYYY-MM,当前值:{plan_month}")
  228. return f"检查失败:日期格式错误,应为 YYYY-MM,实际为 {plan_month}"
  229. if plan_month == current_system_month:
  230. logger.info("检查通过:当前配风计划是当月最新版本")
  231. return f"检查通过:是当月最新版(当前月份:{current_system_month},计划月份:{plan_month})"
  232. else:
  233. logger.warning("检查不通过:当前配风计划不是当月最新版本")
  234. return f"检查不通过:非当月最新版(当前月份:{current_system_month},计划月份:{plan_month})"
  235. @tool
  236. def check_data_by_gas_report(
  237. state: Annotated[dict, InjectedState]
  238. ) -> str:
  239. """
  240. 校验配风计划中瓦斯与二氧化碳数据是否与瓦斯等级鉴定报告一致。
  241. 自动拉取瓦斯鉴定报告,抽取配风计划中的采煤工作面、掘进工作面、
  242. 其他用风地点数据,构造结构化提示交由大模型逐条比对。
  243. """
  244. ventilation_plan = state.get("vent_plan_data", {})
  245. logger.info("===== 开始执行瓦斯、二氧化碳数据一致性校验 =====")
  246. data_service = VentDataService()
  247. try:
  248. mine_name = _extract_mine_name(ventilation_plan)
  249. if not mine_name:
  250. raise ValueError("矿井名称为空")
  251. except Exception as e:
  252. err = "无法从配风计划中获取矿井名称"
  253. logger.error(f"{err}:{str(e)}")
  254. return f"校验失败:{err}"
  255. logger.info("正在调用接口获取瓦斯等级鉴定报告...")
  256. gas_identification_report = data_service.get_gas_identify(mine_name)
  257. # 构造校验提示词
  258. prompt = f"""
  259. 你是专业的煤矿通风瓦斯、二氧化碳数据校验专家,请严格执行【配风计划基础数据一致性审查】。
  260. 请将以下配风计划中的瓦斯、二氧化碳数据进行逐项比对,输出结构化校验结果。
  261. ============================
  262. 【第一份数据:配风计划】
  263. "采煤工作面":{ventilation_plan.get("coal_faces", [])}
  264. "掘进工作面":{ventilation_plan.get("tunneling_faces", [])}
  265. "其他用风地点": {ventilation_plan.get("other_points", [])}
  266. 【第二份数据:瓦斯等级鉴定报告】
  267. "检测地点":{gas_identification_report.get("result", [])}
  268. ============================
  269. 【校验规则 1 —— 瓦斯数据一致性】
  270. 1. 平均绝对瓦斯涌出量 qcg 必须与 瓦斯等级鉴定报告avg_abs_gas 一致
  271. 2. 平均绝对二氧化碳涌出量 qcc 必须与 瓦斯等级鉴定报告 avg_abs_co2 一致
  272. 3. 瓦斯/二氧化碳不均匀系数kcg/kcc 必须与 瓦斯等级鉴定报告gas_uneven_coeff/co2_uneven_coeff 一致
  273. 若地点无鉴定报告,提示:未找到XXX地点的瓦斯/二氧化碳鉴定报告,请重新上传。
  274. 【输出要求】
  275. 1. 按分类逐条输出;2. 标注 通过/不一致/缺失;3. 语言专业简洁。
  276. 请开始校验:
  277. """
  278. logger.info("已生成数据校验提示词,交付大模型进行比对")
  279. return f"""
  280. ==== 配风计划基础数据一致性检查 ====
  281. 已完成数据获取:
  282. - 矿井名称:{mine_name}
  283. - 配风计划:已加载
  284. - 瓦斯鉴定报告:{'已获取' if gas_identification_report else '获取失败/为空'}
  285. 【请大模型基于以下规则与数据执行比对校验】
  286. {prompt}
  287. """
  288. @tool
  289. def check_data_by_face_design(
  290. state: Annotated[dict, InjectedState]
  291. ) -> str:
  292. """
  293. 校验配风计划工作面参数是否与作业规程一致。
  294. 拉取工作面作业规程,比对工作面长度、平均采高、有效断面积等参数,
  295. 构造结构化提示交由大模型逐项校验。
  296. """
  297. ventilation_plan = state.get("vent_plan_data", {})
  298. logger.info("===== 开始执行工作面设计规程一致性校验 =====")
  299. data_service = VentDataService()
  300. try:
  301. mine_name = _extract_mine_name(ventilation_plan)
  302. if not mine_name:
  303. raise ValueError("矿井名称为空")
  304. except Exception as e:
  305. err = "无法从配风计划中获取矿井名称"
  306. logger.error(f"{err}:{str(e)}")
  307. return f"校验失败:{err}"
  308. logger.info("正在调用接口获取工作面作业规程...")
  309. face_design_documents = data_service.get_coal_working_rule(mine_name)
  310. prompt = f"""
  311. 你是专业的煤矿通风数据校验专家,请执行【配风计划基础数据一致性审查】。
  312. ============================
  313. 【第一份数据:配风计划】
  314. {ventilation_plan.get("coal_faces", [])}
  315. 【第二份数据:工作面作业规程】
  316. {face_design_documents}
  317. ============================
  318. 【校验规则】
  319. 1. 工作面长度 face_length 与作业规程 faceLength 一致
  320. 2. 平均采高 average_mining_height 与作业规程 avgMiningHeight 一致
  321. 3. 平均有效断面积 scf 与作业 avg_section_area 一致
  322. 缺失文档则提示:缺少xxx工作面作业规程。
  323. 【输出要求】
  324. 逐条校验、标注结果、最后给出总体合格/不合格。
  325. 请开始校验:
  326. """
  327. logger.info("已生成数据校验提示词,交付大模型进行比对")
  328. return f"""
  329. ==== 配风计划基础数据一致性检查 ====
  330. 已完成数据获取:
  331. - 矿井名称:{mine_name}
  332. - 配风计划:已加载
  333. - 工作面作业规程:{'已获取' if len(face_design_documents) > 0 else '获取失败/为空'}
  334. 【请大模型基于以下规则与数据执行比对校验】
  335. {prompt}
  336. """
  337. @tool
  338. def check_data_by_vent_report(
  339. state: Annotated[dict, InjectedState]
  340. ) -> str:
  341. """
  342. 核验配风计划工作面风速是否与测风报表温度数据匹配。
  343. 拉取当月测风报表,提取各测点的温度数据,按温度区间映射标准风速范围,
  344. 构造结构化提示交由大模型比对工作面风速是否处于合理区间。
  345. """
  346. ventilation_plan = state.get("vent_plan_data", {})
  347. logger.info("===== 开始执行测风报表温度一致性校验 =====")
  348. data_service = VentDataService()
  349. try:
  350. mine_basic_info = ventilation_plan.get("mine_info", {})
  351. mine_name = mine_basic_info.get("mine_name", "")
  352. plan_month = mine_basic_info.get("vent_date", "")
  353. if not mine_name:
  354. raise ValueError("矿井名称为空")
  355. except Exception as e:
  356. err = "无法从配风计划基础信息获取数据"
  357. logger.error(f"{err}:{str(e)}")
  358. return f"校验失败:{err}"
  359. logger.info("正在调用接口获取测风报表...")
  360. ventilation_report = data_service.get_vent_report_data(mine_name, plan_month)
  361. if not ventilation_report:
  362. return f"未获取到{mine_name}的{plan_month}的测风报表"
  363. vent_report_result = ventilation_report.get("result", {})
  364. if not vent_report_result:
  365. return f"未获取到{mine_name}的{plan_month}的测风报表"
  366. measurement_points = vent_report_result.get("testPointList", [])
  367. velocity_records: list[dict[str, Any]] = []
  368. for point in measurement_points:
  369. velocity_record: dict[str, Any] = {
  370. "point_name": point.get("pointName", ""),
  371. "point_desc": point.get("pointDesc", ""),
  372. "temp_c": point.get("tempC", 0.0),
  373. "coal_v": map_temperature_to_velocity_range(point.get("tempC", 0.0))
  374. }
  375. velocity_records.append(velocity_record)
  376. prompt = f"""
  377. 你是专业煤矿通风校验专家,根据测风报表温度校验工作面风速合理性。
  378. ============================
  379. 【第一份数据:配风计划】
  380. {ventilation_plan.get("coal_faces", [])}
  381. 【第二份数据:测风报表温度数据】
  382. {velocity_records}
  383. ============================
  384. 【校验规则】
  385. 1. 工作面风速 vcf 匹配对应温度区间标准风速范围
  386. 2. 比对 average_temperature 与报表 temp_c
  387. 缺失数据提示对应地点缺失报表。
  388. 【输出要求】
  389. 逐条标注:通过/不合理/缺失,清晰说明偏差。
  390. 请开始校验:
  391. """
  392. logger.info("已生成数据校验提示词,交付大模型进行比对")
  393. return f"""
  394. ==== 配风计划基础数据一致性检查 ====
  395. 已完成数据获取:
  396. - 矿井名称:{mine_name}
  397. - 配风计划:已加载
  398. - 测风报表当月温度数据:{'已获取' if velocity_records else '获取失败'}
  399. 【请大模型基于以下规则与数据执行比对校验】
  400. {prompt}
  401. """
  402. # ==================== 本地调试代码(仅本地运行,线上Agent不执行) ====================
  403. if __name__ == '__main__':
  404. with open(r"D:\workspace\py\vent_agent\check_agent\配风计划(新).json", encoding="utf-8") as f:
  405. data = json.load(f)
  406. # 模拟 InjectedState 注入的状态对象
  407. mock_state = {"vent_plan_data": data}
  408. # 调试示例
  409. res = form_review(state=mock_state)
  410. print(res)