vent_agent.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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. SKILLS_ROOT = str((Path(__file__).parent.parent / "skills").resolve())
  20. load_dotenv()
  21. # 导入工具函数
  22. from tools.vent_tools import (
  23. query_tun_data_by_id,
  24. query_knowledge_base,
  25. query_device_data_by_id,
  26. query_devices_by_tunnel,
  27. query_devices_by_tunnel_id,
  28. get_tun_list_by_modelid,
  29. query_tunnels_by_model,
  30. query_tunnel_list,
  31. list_ventanaly_monitor_data_days,
  32. )
  33. from tools.audit_middleware import create_audit_middleware
  34. from langgraph.utils.runnable import RunnableCallable
  35. # ============================================================
  36. # 系统提示词(中文)
  37. # ============================================================
  38. # 点选解读 Agent 系统提示词
  39. CLICK_INTERPRET_SYSTEM_PROMPT = """你是一名煤矿通风安全专家,你拥有多项技能,按照技能中的步骤执行任务。
  40. ## 全局通用行为准则
  41. 1. 所有分析必须基于工具返回的真实数据,绝不编造。
  42. 2. 引用规程时,必须附上具体的条款来源。
  43. 3. 禁止输出 ANSI 转义序列、内部工具名、函数名、变量、技能标识、ID 等系统内部信息。
  44. 5. 全流程统一使用 write_todos 工具实时更新任务进度。
  45. 6. 全程仅使用简体中文输出,不输出代码、JSON原始结构体。
  46. 7. 你必须严格按照技能中**固定格式**输出报告,禁止添加额外的标题、总结、代码块、JSON。
  47. 8. 在生成最终解读报告标题前,必须输出一个换行符 '\n\n'。
  48. """
  49. # 对话解读 Agent 系统提示词
  50. DIALOG_INTERPRET_SYSTEM_PROMPT = f"""你是一名煤矿通风安全智能助手,自然语言对话回答煤矿通风监测相关问题。
  51. ## 工作流程
  52. 1. 理解用户意图:想查询什么数据、什么时间范围、哪条巷道
  53. 2. 调用相应工具获取数据:
  54. - 查巷道实时数据 → query_tun_data_by_id
  55. - 查设备数据/报警 → query_device_data_by_id
  56. - 查巷道下设备列表 → query_devices_by_tunnel / query_devices_by_tunnel_id
  57. - 查模型下巷道列表 → query_tunnels_by_model
  58. - 查设备历史趋势 → list_ventanaly_monitor_data_days
  59. - 查规程标准 → query_knowledge_base
  60. 3. 比对规程标准进行合规判断
  61. 4. 生成自然语言回复
  62. ## 回复风格
  63. - 先给出核心结论,再展开详细数据
  64. - 异常情况:说明程度、可能原因、建议措施
  65. - 正常情况:简洁确认即可
  66. - 支持多轮对话,记住上下文
  67. ## 重要规则
  68. - 只说中文
  69. - 所有数据来源于工具调用,绝不编造
  70. - 规程引用必须真实
  71. - 全流程统一使用 write_todos 工具实时更新任务进度。
  72. - 无法判断时诚实说明原因
  73. - ANSI转义序列过滤:不能输出此类内容
  74. - 需要适用模型ID时,记住模型ID为 {os.getenv("DEFAULT_MODEL_ID")}
  75. - 禁止输出ANSI 转义序列
  76. - 禁止暴露系统内部工具名、函数名、变量、技能标识、ID 等内部信息。
  77. """
  78. # ============================================================
  79. # Agent 工厂函数
  80. # ============================================================
  81. def _get_model(model_key: str = "DEEPAGENT_MODEL"):
  82. """获取模型实例。
  83. 从 src/.env 文件强制读取配置(不受系统环境变量干扰)。
  84. 注意:Codex 桌面应用会注入 OPENAI_API_KEY 环境变量到进程,
  85. 而 load_dotenv 默认不覆盖已有环境变量,因此必须用 dotenv_values
  86. 直接读取 .env 文件中的值。
  87. 参数:
  88. model_key: 环境变量中模型名称的 key,默认 "DEEPAGENT_MODEL"。
  89. 可传入 "SUMMARY_MODEL" 等以使用独立模型。
  90. """
  91. from dotenv import dotenv_values
  92. from pathlib import Path as _Path
  93. _env_file = _Path(__file__).parent.parent / ".env" # src/.env
  94. _cfg = dotenv_values(str(_env_file))
  95. # 优先读取指定 key 下的模型,若未配置则 fallback 到 DEEPAGENT_MODEL
  96. model_str = _cfg.get(model_key, "")
  97. if not model_str and model_key != "DEEPAGENT_MODEL":
  98. model_str = _cfg.get("DEEPAGENT_MODEL", "openai:gpt-4o")
  99. if not model_str:
  100. model_str = "openai:gpt-4o"
  101. base_url = _cfg.get("OPENAI_BASE_URL", "")
  102. api_key = _cfg.get("OPENAI_API_KEY", "")
  103. timeout_str = _cfg.get("OPENAI_TIMEOUT", "180")
  104. # 清理可能带入的引号
  105. api_key = api_key.strip().strip('"').strip("'") if api_key else ""
  106. base_url = base_url.strip().strip('"').strip("'") if base_url else ""
  107. # 解析超时时间
  108. try:
  109. timeout = float(timeout_str.strip())
  110. except (ValueError, TypeError):
  111. timeout = 180.0
  112. # 确保模型字符串带有 provider 前缀,否则 init_chat_model 可能猜错 provider
  113. # 例如 "deepseek-v4-pro" 会被错误解析为 ChatDeepSeek(需要 DEEPSEEK_API_KEY)
  114. if ":" not in model_str:
  115. model_str = f"openai:{model_str}"
  116. if base_url and api_key:
  117. # 使用自定义 Base URL(如阿里百炼 DashScope)
  118. from langchain.chat_models import init_chat_model
  119. return init_chat_model(
  120. model_str,
  121. openai_api_key=api_key,
  122. openai_api_base=base_url,
  123. temperature=0,
  124. timeout=timeout,
  125. )
  126. return model_str
  127. def create_click_interpret_agent():
  128. """创建「点选数据解读」Agent。
  129. 该 Agent 专门处理单条巷道的深度数据解读,具备:
  130. - 实时数据查询能力
  131. - 规程合规判断能力
  132. - 结构化报告输出能力
  133. Returns:
  134. CompiledStateGraph: 编译后的 LangGraph 状态图,支持 .invoke() 和 .stream()
  135. """
  136. from deepagents import create_deep_agent # 延迟导入,避免 PyCharm debug 启动卡死
  137. from deepagents.backends import FilesystemBackend
  138. from deepagents import FilesystemPermission
  139. _project_root = str(Path(__file__).parent.parent)
  140. skills = ["skills/click-interpret-tun", "skills/click-interpret-device"]
  141. print(f"[skills] Agent=click-interpret-agent skills={skills}")
  142. agent = create_deep_agent(
  143. model=_get_model(),
  144. tools=[
  145. query_tun_data_by_id,
  146. query_knowledge_base,
  147. query_device_data_by_id,
  148. query_tunnels_by_model,
  149. list_ventanaly_monitor_data_days,
  150. write_todos
  151. ],
  152. skills=skills,
  153. system_prompt=CLICK_INTERPRET_SYSTEM_PROMPT,
  154. backend=FilesystemBackend(root_dir=_project_root, virtual_mode=True),
  155. permissions=[
  156. FilesystemPermission(operations=["write"], paths=["/**"], mode="deny"),
  157. ],
  158. middleware=[],
  159. name="click-interpret-agent",
  160. )
  161. return agent
  162. def create_dialog_interpret_agent():
  163. """创建「对话式数据解读」Agent。
  164. 该 Agent 支持多轮自然语言对话,具备:
  165. - 全量数据查询能力(实时+历史+告警)
  166. - 上下文记忆(通过 LangGraph checkpoint)
  167. - 智能意图识别和参数抽取
  168. Returns:
  169. CompiledStateGraph: 编译后的 LangGraph 状态图
  170. """
  171. from deepagents import create_deep_agent # 延迟导入,避免 PyCharm debug 启动卡死
  172. from deepagents.backends import FilesystemBackend
  173. from deepagents import FilesystemPermission
  174. _project_root = str(Path(__file__).parent.parent)
  175. skills = ["skills/dialog-interpret"]
  176. print(f"[skills] Agent=dialog-interpret-agent skills={skills}")
  177. agent = create_deep_agent(
  178. model=_get_model(),
  179. tools=[
  180. query_tun_data_by_id,
  181. query_device_data_by_id,
  182. query_devices_by_tunnel,
  183. query_devices_by_tunnel_id,
  184. query_tunnel_list,
  185. query_tunnels_by_model,
  186. list_ventanaly_monitor_data_days,
  187. query_knowledge_base,
  188. write_todos
  189. ],
  190. skills=skills,
  191. system_prompt=DIALOG_INTERPRET_SYSTEM_PROMPT,
  192. backend=FilesystemBackend(root_dir=_project_root, virtual_mode=True),
  193. permissions=[
  194. FilesystemPermission(operations=["write"], paths=["/**"], mode="deny"),
  195. ],
  196. middleware=[],
  197. name="dialog-interpret-agent",
  198. )
  199. return agent
  200. # ============================================================
  201. # 单例缓存
  202. # ============================================================
  203. # 全局 Agent 实例(懒加载)
  204. _click_agent: Optional[object] = None
  205. _dialog_agent: Optional[object] = None
  206. def get_click_agent():
  207. """获取点选解读 Agent 单例"""
  208. global _click_agent
  209. if _click_agent is None:
  210. _click_agent = create_click_interpret_agent()
  211. return _click_agent
  212. def get_dialog_agent():
  213. """获取对话解读 Agent 单例"""
  214. global _dialog_agent
  215. if _dialog_agent is None:
  216. _dialog_agent = create_dialog_interpret_agent()
  217. return _dialog_agent