vent_agent.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. # -*- coding: utf-8 -*-
  2. """
  3. DeepAgents Agent 创建与配置模块
  4. 核心功能:
  5. - 创建主 DeepAgent(整合 Skills + Tools + MCP 数据)
  6. - 配置流式输出
  7. - 管理 Agent 实例生命周期
  8. 架构:
  9. - 模型: OpenAI GPT 系列(通过环境变量 OPENAI_API_KEY 配置)
  10. - Skills: 渐进式加载(点选解读 / 对话解读)
  11. - Tools: 5个数据查询工具(query_laneway_data / query_regulation 等)
  12. - Backend: FilesystemBackend(virtual_mode,用于加载本地技能文件;写操作已禁用)
  13. """
  14. import os
  15. from pathlib import Path
  16. from typing import Optional
  17. from dotenv import load_dotenv
  18. from langchain.agents.middleware.todo import write_todos
  19. from langgraph.checkpoint.memory import MemorySaver
  20. SKILLS_ROOT = str((Path(__file__).parent.parent / "skills").resolve())
  21. load_dotenv()
  22. # 导入工具函数
  23. from tools.vent_tools import (
  24. query_tun_data_by_id,
  25. query_knowledge_base,
  26. query_device_data_by_id,
  27. query_devices_by_tunnel,
  28. query_devices_by_tunnel_id,
  29. get_tun_list_by_modelid,
  30. query_tunnels_by_model,
  31. query_tunnel_list,
  32. list_ventanaly_monitor_data_days,
  33. get_device_kind_dict,
  34. get_device_list_by_kind,
  35. query_device_realtime_data,
  36. get_needq_all_data,
  37. save_user_preference,
  38. list_user_preferences,
  39. delete_user_preference,
  40. request_plan_approval,
  41. )
  42. from tools.audit_middleware import create_audit_middleware
  43. from tools.context_tracker import init_system_components
  44. from langgraph.utils.runnable import RunnableCallable
  45. # 导入需风量计算工具
  46. from tools.calc_tools import (
  47. calc_face_by_gas,
  48. calc_face_by_workers,
  49. calc_face_by_wind_speed,
  50. calc_face_air_volume_max,
  51. calc_tunnel_by_gas,
  52. calc_tunnel_by_explosives,
  53. calc_tunnel_by_workers,
  54. calc_tunnel_by_wind_speed,
  55. calc_tunnel_air_volume_max,
  56. calc_chamber_by_equipment,
  57. calc_chamber_by_wind_speed,
  58. calc_other_by_wind_speed,
  59. calc_effective_area,
  60. calc_total_air_volume,
  61. )
  62. # ============================================================
  63. # 系统提示词(中文)
  64. # ============================================================
  65. # 点选解读 Agent 系统提示词
  66. CLICK_INTERPRET_SYSTEM_PROMPT = """你是一名煤矿通风安全专家,你拥有多项技能,按照技能中的步骤执行任务。
  67. ## 全局通用行为准则
  68. 1. 所有分析必须基于工具返回的真实数据,绝不编造。
  69. 2. 引用规程时,必须附上具体的条款来源。
  70. 3. 禁止输出 ANSI 转义序列、内部工具名、函数名、变量、技能标识、ID 等系统内部信息。
  71. 5. 全流程统一使用 write_todos 工具实时更新任务进度。
  72. 6. 全程仅使用简体中文输出,不输出代码、JSON原始结构体。
  73. 7. 你必须严格按照技能中**固定格式**输出报告,禁止添加额外的标题、总结、代码块、JSON。
  74. 8. 在生成最终解读报告标题前,必须输出一个换行符 '\n\n'。
  75. """
  76. # 对话解读 Agent 系统提示词(统一版:合并了对话解读 + 需风量计算能力)
  77. DIALOG_INTERPRET_SYSTEM_PROMPT = f"""你是一名煤矿通风安全智能助手,具备两大核心能力:**通风监测数据解读**和**需风量交互式计算**。根据用户意图自动切换工作模式。
  78. ## 能力一:通风监测数据解读
  79. ### 工作流程
  80. 1. 理解用户意图:想查询什么数据、什么时间范围、哪条巷道
  81. 2. 调用相应工具获取数据:
  82. - 查巷道实时数据 → query_tun_data_by_id
  83. - 查设备数据/报警 → query_device_data_by_id
  84. - 查设备实时快照 → query_device_realtime_data
  85. - 查巷道下设备列表 → query_devices_by_tunnel / query_devices_by_tunnel_id
  86. - 查模型下巷道列表 → query_tunnels_by_model
  87. - 查设备历史趋势 → list_ventanaly_monitor_data_days
  88. - 查规程标准 → query_knowledge_base
  89. - 查设备类型字典 → get_device_kind_dict
  90. - 按设备类型查设备列表 → get_device_list_by_kind
  91. 3. 比对规程标准进行合规判断
  92. 4. 生成自然语言回复
  93. ## 能力二:需风量交互式计算
  94. ### 支持的用风地点类型
  95. - 采煤工作面(含备用工作面)、掘进工作面、机电硐室
  96. - 其他用风地点(主要进回风巷、采区进回风巷、其他通风人行巷道等)
  97. - 多地点汇总、通防管控平台数据查询
  98. ### 计算铁律(红线)
  99. 1. **必须调用工具计算**:所有需风量计算必须通过调用 calc_tools 中的工具函数完成,绝对禁止凭 LLM 知识直接给出计算结果。
  100. 2. **参数缺失要追问**:若用户未提供必要计算参数,明确列出所需参数并引导用户补充,绝不编造参数值。
  101. 3. **列式计算**:展示每个计算过程的公式 → 代入数值 → 计算结果,不能只给结果。
  102. 4. **多轮对话承接**:记住当前会话中的用风地点类型和已有参数,用户补充参数或要求调整时自动衔接。
  103. ## 回复风格
  104. - 先给出核心结论,再展开详细数据
  105. - 异常情况:说明程度、可能原因、建议措施
  106. - 正常情况:简洁确认即可
  107. - 支持多轮对话,记住上下文
  108. ## 重要规则
  109. - 只说中文
  110. - 所有数据来源于工具调用,绝不编造
  111. - 规程引用必须真实,引用时附上具体条款来源(《煤矿安全规程》2025版、AQ 1056-2008)
  112. - 全流程统一使用 write_todos 工具实时更新任务进度。
  113. - 无法判断时诚实说明原因
  114. - ANSI转义序列过滤:不能输出此类内容
  115. - 需要适用模型ID时,记住模型ID为 {os.getenv("DEFAULT_MODEL_ID")}
  116. - 禁止输出ANSI 转义序列
  117. - 禁止暴露系统内部工具名、函数名、变量、技能标识、ID 等内部信息。
  118. - 严格按照技能(skill: dialog-interpret / needq-calc)中定义的流程执行任务
  119. ## 能力三:用户偏好记忆
  120. - 当用户明确要求"记住""保存为习惯""这是我的偏好""以后都用这个"时,调用 save_user_preference 保存到个人记忆库
  121. - 当用户询问"我的偏好""有哪些习惯""查看我的偏好"时,调用 list_user_preferences 查看
  122. - 当用户要求"删除某条习惯""忘掉那个偏好"时,先调用 list_user_preferences 确认ID,再调用 delete_user_preference 删除
  123. - 保存偏好时,content 字段应精炼概括用户的要求(一句话),keywords 字段列出相关关键词
  124. - 系统已自动将用户偏好注入到每条消息前缀中,请主动参考这些偏好来个性化回复
  125. """
  126. # ============================================================
  127. # Agent 工厂函数
  128. # ============================================================
  129. def _get_model(model_key: str = "DEEPAGENT_MODEL"):
  130. """获取模型实例。
  131. 从 src/.env 文件强制读取配置(不受系统环境变量干扰)。
  132. 注意:Codex 桌面应用会注入 OPENAI_API_KEY 环境变量到进程,
  133. 而 load_dotenv 默认不覆盖已有环境变量,因此必须用 dotenv_values
  134. 直接读取 .env 文件中的值。
  135. 参数:
  136. model_key: 环境变量中模型名称的 key,默认 "DEEPAGENT_MODEL"。
  137. 可传入 "SUMMARY_MODEL" 等以使用独立模型。
  138. """
  139. from dotenv import dotenv_values
  140. from pathlib import Path as _Path
  141. _env_file = _Path(__file__).parent.parent / ".env" # src/.env
  142. _cfg = dotenv_values(str(_env_file))
  143. # 优先读取指定 key 下的模型,若未配置则 fallback 到 DEEPAGENT_MODEL
  144. model_str = _cfg.get(model_key, "")
  145. if not model_str and model_key != "DEEPAGENT_MODEL":
  146. model_str = _cfg.get("DEEPAGENT_MODEL", "openai:gpt-4o")
  147. if not model_str:
  148. model_str = "openai:gpt-4o"
  149. base_url = _cfg.get("OPENAI_BASE_URL", "")
  150. api_key = _cfg.get("OPENAI_API_KEY", "")
  151. timeout_str = _cfg.get("OPENAI_TIMEOUT", "180")
  152. # 清理可能带入的引号
  153. api_key = api_key.strip().strip('"').strip("'") if api_key else ""
  154. base_url = base_url.strip().strip('"').strip("'") if base_url else ""
  155. # 解析超时时间
  156. try:
  157. timeout = float(timeout_str.strip())
  158. except (ValueError, TypeError):
  159. timeout = 180.0
  160. # 确保模型字符串带有 provider 前缀,否则 init_chat_model 可能猜错 provider
  161. # 例如 "deepseek-v4-pro" 会被错误解析为 ChatDeepSeek(需要 DEEPSEEK_API_KEY)
  162. if ":" not in model_str:
  163. model_str = f"openai:{model_str}"
  164. if base_url and api_key:
  165. # 使用自定义 Base URL(如阿里百炼 DashScope)
  166. from langchain.chat_models import init_chat_model
  167. return init_chat_model(
  168. model_str,
  169. openai_api_key=api_key,
  170. openai_api_base=base_url,
  171. temperature=0,
  172. timeout=timeout,
  173. )
  174. return model_str
  175. def create_click_interpret_agent():
  176. """创建「点选数据解读」Agent。
  177. 该 Agent 专门处理单条巷道的深度数据解读,具备:
  178. - 实时数据查询能力
  179. - 规程合规判断能力
  180. - 结构化报告输出能力
  181. Returns:
  182. CompiledStateGraph: 编译后的 LangGraph 状态图,支持 .invoke() 和 .stream()
  183. """
  184. from deepagents import create_deep_agent # 延迟导入,避免 PyCharm debug 启动卡死
  185. from deepagents.backends import FilesystemBackend
  186. from deepagents import FilesystemPermission
  187. _project_root = str(Path(__file__).parent.parent)
  188. skills = ["skills/click-interpret-tun", "skills/click-interpret-device"]
  189. print(f"[skills] Agent=click-interpret-agent skills={skills}")
  190. agent = create_deep_agent(
  191. model=_get_model(),
  192. tools=[
  193. query_tun_data_by_id,
  194. query_knowledge_base,
  195. query_device_data_by_id,
  196. query_tunnels_by_model,
  197. list_ventanaly_monitor_data_days,
  198. write_todos
  199. ],
  200. skills=skills,
  201. system_prompt=CLICK_INTERPRET_SYSTEM_PROMPT,
  202. backend=FilesystemBackend(root_dir=_project_root, virtual_mode=True),
  203. permissions=[
  204. FilesystemPermission(operations=["write"], paths=["/**"], mode="deny"),
  205. ],
  206. middleware=[],
  207. name="click-interpret-agent",
  208. checkpointer=MemorySaver(),
  209. )
  210. return agent
  211. def create_dialog_interpret_agent():
  212. """创建「对话式数据解读」Agent。
  213. 该 Agent 支持多轮自然语言对话,具备:
  214. - 全量数据查询能力(实时+历史+告警)
  215. - 上下文记忆(通过 LangGraph checkpoint)
  216. - 智能意图识别和参数抽取
  217. - 使用运行时模型配置(支持前端动态切换)
  218. Returns:
  219. CompiledStateGraph: 编译后的 LangGraph 状态图
  220. """
  221. from deepagents import create_deep_agent # 延迟导入,避免 PyCharm debug 启动卡死
  222. from deepagents.backends import FilesystemBackend
  223. from deepagents import FilesystemPermission
  224. from api.model_config import get_model_instance
  225. _project_root = str(Path(__file__).parent.parent)
  226. skills = ["skills/"] # 父目录模式:SkillsMiddleware 自动扫描 skills/ 下所有子目录(含 SKILL.md 的技能目录)
  227. print(f"[skills] Agent=dialog-interpret-agent skills={skills}")
  228. agent = create_deep_agent(
  229. model=get_model_instance(),
  230. tools=[
  231. # 数据查询工具
  232. query_tun_data_by_id,
  233. query_device_data_by_id,
  234. query_devices_by_tunnel,
  235. query_devices_by_tunnel_id,
  236. query_tunnel_list,
  237. query_tunnels_by_model,
  238. list_ventanaly_monitor_data_days,
  239. query_knowledge_base,
  240. get_device_kind_dict,
  241. get_device_list_by_kind,
  242. query_device_realtime_data,
  243. # 基础工具
  244. write_todos,
  245. # 辅助计算工具
  246. calc_effective_area,
  247. calc_total_air_volume,
  248. # 采煤工作面计算
  249. calc_face_by_gas,
  250. calc_face_by_workers,
  251. calc_face_by_wind_speed,
  252. calc_face_air_volume_max,
  253. # 掘进工作面计算
  254. calc_tunnel_by_gas,
  255. calc_tunnel_by_explosives,
  256. calc_tunnel_by_workers,
  257. calc_tunnel_by_wind_speed,
  258. calc_tunnel_air_volume_max,
  259. # 硐室计算
  260. calc_chamber_by_equipment,
  261. calc_chamber_by_wind_speed,
  262. # 其他巷道计算
  263. calc_other_by_wind_speed,
  264. # MCP 远程数据查询
  265. get_needq_all_data,
  266. # 用户偏好记忆
  267. save_user_preference,
  268. list_user_preferences,
  269. delete_user_preference,
  270. # 计划审批(Human-in-the-Loop)
  271. request_plan_approval,
  272. ],
  273. skills=skills,
  274. system_prompt=DIALOG_INTERPRET_SYSTEM_PROMPT,
  275. backend=FilesystemBackend(root_dir=_project_root, virtual_mode=True),
  276. permissions=[
  277. FilesystemPermission(operations=["write"], paths=["/**"], mode="deny"),
  278. ],
  279. middleware=[],
  280. name="dialog-interpret-agent",
  281. checkpointer=MemorySaver(),
  282. )
  283. # ── 初始化上下文用量追踪 ──
  284. _init_context_tracker_for_dialog()
  285. return agent
  286. def _init_context_tracker_for_dialog():
  287. """为对话 agent 初始化系统组件 token 预估(统一版:含对话工具 + 计算工具 + MCP)。"""
  288. from tools.context_tracker import init_system_components, _tool_to_text
  289. _all_tools = [
  290. # 数据查询工具
  291. query_tun_data_by_id, query_device_data_by_id,
  292. query_devices_by_tunnel, query_devices_by_tunnel_id,
  293. query_tunnel_list, query_tunnels_by_model,
  294. list_ventanaly_monitor_data_days, query_knowledge_base,
  295. get_device_kind_dict, get_device_list_by_kind,
  296. query_device_realtime_data, write_todos,
  297. # 辅助计算
  298. calc_effective_area, calc_total_air_volume,
  299. # 采煤面
  300. calc_face_by_gas, calc_face_by_workers,
  301. calc_face_by_wind_speed, calc_face_air_volume_max,
  302. # 掘进面
  303. calc_tunnel_by_gas, calc_tunnel_by_explosives,
  304. calc_tunnel_by_workers, calc_tunnel_by_wind_speed,
  305. calc_tunnel_air_volume_max,
  306. # 硐室
  307. calc_chamber_by_equipment, calc_chamber_by_wind_speed,
  308. # 其他
  309. calc_other_by_wind_speed,
  310. # 用户偏好
  311. save_user_preference, list_user_preferences, delete_user_preference,
  312. # 计划审批
  313. request_plan_approval,
  314. ]
  315. tool_texts = [_tool_to_text(t) for t in _all_tools]
  316. # MCP 工具单独归类
  317. mcp_text = _tool_to_text(get_needq_all_data)
  318. # 读取技能文件内容(动态扫描 skills/ 下所有技能,不再硬编码列表)
  319. skill_contents = []
  320. skills_root = Path(__file__).parent.parent / "skills"
  321. if skills_root.is_dir():
  322. for skill_dir in sorted(skills_root.iterdir()):
  323. if not skill_dir.is_dir() or skill_dir.name.startswith(".") or skill_dir.name == "__pycache__":
  324. continue
  325. sf = skill_dir / "SKILL.md"
  326. if sf.exists():
  327. skill_contents.append(sf.read_text(encoding="utf-8"))
  328. print(f"[context_tracker] 已加载技能: {skill_dir.name}")
  329. init_system_components(
  330. system_prompt=DIALOG_INTERPRET_SYSTEM_PROMPT,
  331. tool_defs=tool_texts,
  332. skill_contents=skill_contents,
  333. mcp_defs=[mcp_text],
  334. )
  335. # ============================================================
  336. # 单例缓存
  337. # ============================================================
  338. # 全局 Agent 实例(懒加载)
  339. _click_agent: Optional[object] = None
  340. _dialog_agent: Optional[object] = None
  341. def invalidate_dialog_cache():
  342. """失效对话解读 Agent 缓存(模型切换时调用)。
  343. 仅清除 dialog_agent 的缓存,不影响 click_agent(后者继续使用 .env 固定模型)。
  344. """
  345. global _dialog_agent
  346. _dialog_agent = None
  347. print("[模型切换] dialog-interpret-agent 缓存已失效")
  348. def get_click_agent():
  349. """获取点选解读 Agent 单例"""
  350. global _click_agent
  351. if _click_agent is None:
  352. _click_agent = create_click_interpret_agent()
  353. return _click_agent
  354. def get_dialog_agent():
  355. """获取对话解读 Agent 单例"""
  356. global _dialog_agent
  357. if _dialog_agent is None:
  358. _dialog_agent = create_dialog_interpret_agent()
  359. return _dialog_agent