# -*- coding: utf-8 -*- """ DeepAgents Agent 创建与配置模块 核心功能: - 创建主 DeepAgent(整合 Skills + Tools + MCP 数据) - 配置流式输出 - 管理 Agent 实例生命周期 架构: - 模型: OpenAI GPT 系列(通过环境变量 OPENAI_API_KEY 配置) - Skills: 渐进式加载(点选解读 / 对话解读) - Tools: 5个数据查询工具(query_laneway_data / query_regulation 等) - Backend: FilesystemBackend(virtual_mode,用于加载本地技能文件;写操作已禁用) """ import os from pathlib import Path from typing import Optional from dotenv import load_dotenv from langchain.agents.middleware.todo import write_todos from langgraph.checkpoint.memory import MemorySaver SKILLS_ROOT = str((Path(__file__).parent.parent / "skills").resolve()) PROJECT_ROOT = str((Path(__file__).parent.parent).resolve()) load_dotenv() # 导入工具函数 from tools.vent_tools import ( # 数据查询 query_tun_data_by_id, query_knowledge_base, query_device_data, query_device_data_by_id, query_devices_by_tunnel, query_devices_by_tunnel_id, query_devices_by_model, get_tun_list_by_modelid, query_tunnels_by_model, query_tunnel_list, list_ventanaly_monitor_data_days, get_device_kind_dict, get_device_list_by_kind, query_device_realtime_data, get_needq_all_data, get_dict_list_by_dictcode, # 需风量 / 模型 get_model_param_pub_list, get_model_wind, get_sensor_wind, simulate_needq_heading_face, simulate_needq_room, simulate_needq_ret_work_face, simulate_needq_other, # 设备信息 query_device_info, query_device_type_info, query_monitor_params, # 故障诊断 check_model_connect_status, check_model_one_dir_cycle, check_model_one_dir_node, check_model_diagonal_structure, get_model_fault_diagnosis, # 避灾路线 get_escape_path, get_escape_path_each_exit, # 关键阻力 / 压能 get_out_shafts, get_in_shafts, get_max_resistance_path, get_three_area_distribution, get_key_path_decision, get_path_press_power, # 网络解算 net_cal, net_cal_for_plan, # 报警 / 日志 get_alarm_log_history, get_device_set_log_history, get_sys_log_history, # 场景管理 get_manage_system_by_strType, query_system_by_systemID, # 煤矿基础 get_gas_identify_vo, get_by_mine_name, query_control_testWind, # 数据库 / 文件 execute_sql_query, get_file_list_by_type, get_file_base64_by_id, # 报表 get_latest_report, # 用户偏好 save_user_preference, list_user_preferences, delete_user_preference, # 计划审批 request_plan_approval, ) from tools.web_search_tools import web_search, web_fetch from tools.file_reader_tools import read_file_content from tools.time_tools import get_current_time from tools.chat_history_tools import search_chat_history, get_session_chat, get_current_session_id from tools.report_utils import save_report from tools.audit_middleware import create_audit_middleware from tools.context_tracker import init_system_components from langgraph.utils.runnable import RunnableCallable # 导入需风量计算工具 from tools.calc_tools import ( calc_face_by_gas, calc_face_by_workers, calc_face_by_wind_speed, calc_face_air_volume_max, calc_tunnel_by_gas, calc_tunnel_by_explosives, calc_tunnel_by_workers, calc_tunnel_by_wind_speed, calc_tunnel_air_volume_max, calc_chamber_by_equipment, calc_chamber_by_wind_speed, calc_other_by_wind_speed, calc_effective_area, calc_total_air_volume, ) # ============================================================ # 系统提示词(中文) # ============================================================ # 点选解读 Agent 系统提示词 CLICK_INTERPRET_SYSTEM_PROMPT = """你是一名煤矿通风安全专家,你拥有多项技能,按照技能中的步骤执行任务。 ## 全局通用行为准则 1. 所有分析必须基于工具返回的真实数据,绝不编造。 2. 引用规程时,必须附上具体的条款来源。 3. 禁止输出 ANSI 转义序列、内部工具名、函数名、变量、技能标识、ID 等系统内部信息。 5. 全流程统一使用 write_todos 工具实时更新任务进度。 6. 全程仅使用简体中文输出,不输出代码、JSON原始结构体。 7. 你必须严格按照技能中**固定格式**输出报告,禁止添加额外的标题、总结、代码块、JSON。 8. 在生成最终解读报告标题前,必须输出一个换行符 '\n\n'。 """ # 对话解读 Agent 系统提示词(统一版:合并了对话解读 + 需风量计算能力) DIALOG_INTERPRET_SYSTEM_PROMPT = f"""你是一名煤矿通风安全智能助手,具备两大核心能力:**通风监测数据解读**和**需风量交互式计算**。根据用户意图自动切换工作模式。 ## 能力一:通风监测数据解读 ### 工作流程 1. 理解用户意图:想查询什么数据、什么时间范围、哪条巷道 2. 调用相应工具获取数据: - 查巷道实时数据 → query_tun_data_by_id - 查设备数据/报警 → query_device_data_by_id - 查设备实时快照 → query_device_realtime_data - 查巷道下设备列表 → query_devices_by_tunnel / query_devices_by_tunnel_id - 查模型下巷道列表 → query_tunnels_by_model - 查设备历史趋势 → list_ventanaly_monitor_data_days - 查规程标准 → query_knowledge_base - 查设备类型字典 → get_device_kind_dict - 按设备类型查设备列表 → get_device_list_by_kind 3. 比对规程标准进行合规判断 4. 生成自然语言回复 ## 能力二:需风量交互式计算 ### 支持的用风地点类型 - 采煤工作面(含备用工作面)、掘进工作面、机电硐室 - 其他用风地点(主要进回风巷、采区进回风巷、其他通风人行巷道等) - 多地点汇总、通防管控平台数据查询 ### 计算铁律(红线) 1. **必须调用工具计算**:所有需风量计算必须通过调用 calc_tools 中的工具函数完成,绝对禁止凭 LLM 知识直接给出计算结果。 2. **参数缺失要追问**:若用户未提供必要计算参数,明确列出所需参数并引导用户补充,绝不编造参数值。 3. **列式计算**:展示每个计算过程的公式 → 代入数值 → 计算结果,不能只给结果。 4. **多轮对话承接**:记住当前会话中的用风地点类型和已有参数,用户补充参数或要求调整时自动衔接。 ## 回复风格 - 先给出核心结论,再展开详细数据 - 异常情况:说明程度、可能原因、建议措施 - 正常情况:简洁确认即可 - 支持多轮对话,记住上下文 ## 重要规则 - 只说中文 - 所有数据来源于工具调用,绝不编造 - 规程引用必须真实,引用时附上具体条款来源(《煤矿安全规程》2025版、AQ 1056-2008) - 全流程统一使用 write_todos 工具实时更新任务进度。 - 无法判断时诚实说明原因 - ANSI转义序列过滤:不能输出此类内容 - 需要适用模型ID时,记住模型ID为 {os.getenv("DEFAULT_MODEL_ID")} - 禁止输出ANSI 转义序列 - 禁止暴露系统内部工具名、函数名、变量、技能标识、ID 等内部信息。 - 严格按照技能(skill: dialog-interpret / needq-calc)中定义的流程执行任务 ## 能力三:用户偏好记忆 - 当用户明确要求"记住""保存为习惯""这是我的偏好""以后都用这个"时,调用 save_user_preference 保存到个人记忆库 - 当用户询问"我的偏好""有哪些习惯""查看我的偏好"时,调用 list_user_preferences 查看 - 当用户要求"删除某条习惯""忘掉那个偏好"时,先调用 list_user_preferences 确认ID,再调用 delete_user_preference 删除 - 保存偏好时,content 字段应精炼概括用户的要求(一句话),keywords 字段列出相关关键词 - 系统已自动将用户偏好注入到每条消息前缀中,请主动参考这些偏好来个性化回复 ## 能力四:联网搜索 - 当用户问题超出煤矿通风专业知识库覆盖范围时,使用 web_search 查询互联网公开信息 - 获取搜索结果后可按需调用 web_fetch 查看详情页的完整内容 - 搜索优先级:先查内部知识库(query_knowledge_base)→ 知识库信息不足或需要最新政策/行业新闻时再联网搜索 - 引用网络信息时标注来源 URL,说明该信息的时效性和局限性 ## 能力五:文件内容读取 - 当用户上传了文件,消息中会包含「文件临时路径」,请使用 read_file_content 工具读取文件内容 - 支持的文件格式:PDF(.pdf)、Word(.docx)、Excel(.xlsx/.xlsm)、PowerPoint(.pptx)、纯文本(.txt/.md/.csv/.json) - 读取到文件内容后,根据用户的要求进行分析、总结、数据提取或计算 - 如果文件内容为空或格式不支持,向用户说明具体情况 ## 能力六:聊天记录查询 - 当用户询问"之前聊过什么""搜索历史""查看之前的对话""找一下关于xxx的记录"时, 使用 search_chat_history 搜索会话标题,再用 get_session_chat 读取具体内容 - 聊天记录存储在数据库而非文件系统,请使用这两个专用工具,切勿用 ls/read_file/grep 查找 - 搜索到相关会话后,可以总结、引用或提取其中的信息来回答用户 ## 能力七:报告保存 - 当用户要求"生成报告""输出报告""保存为文档""导出分析结果"时, 调用 save_report 工具将 Markdown 内容保存为 .md 文件 - 切勿使用 write_file / edit_file 等文件系统工具写文件(已被权限禁止) - save_report 会自动生成文件名和下载链接,返回给用户即可 """ # ============================================================ # Agent 工厂函数 # ============================================================ def _get_model(model_key: str = "DEEPAGENT_MODEL"): """获取模型实例。 从 src/.env 文件强制读取配置(不受系统环境变量干扰)。 注意:Codex 桌面应用会注入 OPENAI_API_KEY 环境变量到进程, 而 load_dotenv 默认不覆盖已有环境变量,因此必须用 dotenv_values 直接读取 .env 文件中的值。 参数: model_key: 环境变量中模型名称的 key,默认 "DEEPAGENT_MODEL"。 可传入 "SUMMARY_MODEL" 等以使用独立模型。 """ from dotenv import dotenv_values from pathlib import Path as _Path _env_file = _Path(__file__).parent.parent / ".env" # src/.env _cfg = dotenv_values(str(_env_file)) # 优先读取指定 key 下的模型,若未配置则 fallback 到 DEEPAGENT_MODEL model_str = _cfg.get(model_key, "") if not model_str and model_key != "DEEPAGENT_MODEL": model_str = _cfg.get("DEEPAGENT_MODEL", "openai:gpt-4o") if not model_str: model_str = "openai:gpt-4o" base_url = _cfg.get("OPENAI_BASE_URL", "") api_key = _cfg.get("OPENAI_API_KEY", "") timeout_str = _cfg.get("OPENAI_TIMEOUT", "180") # 清理可能带入的引号 api_key = api_key.strip().strip('"').strip("'") if api_key else "" base_url = base_url.strip().strip('"').strip("'") if base_url else "" # 解析超时时间 try: timeout = float(timeout_str.strip()) except (ValueError, TypeError): timeout = 180.0 # 确保模型字符串带有 provider 前缀,否则 init_chat_model 可能猜错 provider # 例如 "deepseek-v4-pro" 会被错误解析为 ChatDeepSeek(需要 DEEPSEEK_API_KEY) if ":" not in model_str: model_str = f"openai:{model_str}" if base_url and api_key: # 使用自定义 Base URL(如阿里百炼 DashScope) from langchain.chat_models import init_chat_model return init_chat_model( model_str, openai_api_key=api_key, openai_api_base=base_url, temperature=0, timeout=timeout, ) return model_str def create_click_interpret_agent(): """创建「点选数据解读」Agent。 该 Agent 专门处理单条巷道的深度数据解读,具备: - 实时数据查询能力 - 规程合规判断能力 - 结构化报告输出能力 Returns: CompiledStateGraph: 编译后的 LangGraph 状态图,支持 .invoke() 和 .stream() """ from deepagents import create_deep_agent # 延迟导入,避免 PyCharm debug 启动卡死 from deepagents.backends import FilesystemBackend from deepagents import FilesystemPermission # _project_root = str(Path(__file__).parent.parent) skills = [SKILLS_ROOT + "/click-interpret-tun", SKILLS_ROOT + "/click-interpret-device"] print(f"[skills] Agent=click-interpret-agent skills={skills}") agent = create_deep_agent( model=_get_model(), tools=[ query_tun_data_by_id, query_knowledge_base, query_device_data_by_id, query_tunnels_by_model, list_ventanaly_monitor_data_days, write_todos ], skills=skills, system_prompt=CLICK_INTERPRET_SYSTEM_PROMPT, backend=FilesystemBackend(root_dir=PROJECT_ROOT, virtual_mode=True), permissions=[ FilesystemPermission(operations=["write"], paths=["/**"], mode="deny"), ], middleware=[], name="click-interpret-agent", checkpointer=MemorySaver(), ) return agent def create_dialog_interpret_agent(): """创建「对话式数据解读」Agent。 该 Agent 支持多轮自然语言对话,具备: - 全量数据查询能力(实时+历史+告警) - 上下文记忆(通过 LangGraph checkpoint) - 智能意图识别和参数抽取 - 使用运行时模型配置(支持前端动态切换) Returns: CompiledStateGraph: 编译后的 LangGraph 状态图 """ from deepagents import create_deep_agent # 延迟导入,避免 PyCharm debug 启动卡死 from deepagents.backends import FilesystemBackend from deepagents import FilesystemPermission from api.model_config import get_model_instance # ── 动态注入当前日期到系统提示词 ── from datetime import datetime, timezone, timedelta _china_tz = timezone(timedelta(hours=8)) _now = datetime.now(_china_tz) _dated_prompt = ( DIALOG_INTERPRET_SYSTEM_PROMPT + f"\n## 系统时间\n当前日期时间:{_now.strftime('%Y年%m月%d日 %H:%M:%S')}(中国标准时间 CST,UTC+8)。\n" + f"今天是 {_now.strftime('%Y')}年{_now.strftime('%m')}月{_now.strftime('%d')}日,周{'一二三四五六日'[_now.weekday()]}。\n" + "所有涉及日期、时间的判断必须以本系统时间为准,不要使用你自己的训练数据中的日期。" ) skills = [SKILLS_ROOT] # 绝对路径:SkillsMiddleware 自动扫描下所有子目录(含 SKILL.md 的技能目录) print(f"[skills] Agent=dialog-interpret-agent skills={skills}") print(f"[日期] 已注入当前系统时间: {_now.strftime('%Y-%m-%d %H:%M:%S')} CST") agent = create_deep_agent( model=get_model_instance(), tools=[ # ── 数据查询工具 ── query_tun_data_by_id, query_device_data, query_device_data_by_id, query_devices_by_tunnel, query_devices_by_tunnel_id, query_devices_by_model, query_tunnel_list, query_tunnels_by_model, get_tun_list_by_modelid, list_ventanaly_monitor_data_days, query_knowledge_base, get_device_kind_dict, get_device_list_by_kind, query_device_realtime_data, get_needq_all_data, get_dict_list_by_dictcode, # ── 需风量 / 模型数据 ── get_model_param_pub_list, get_model_wind, get_sensor_wind, simulate_needq_heading_face, simulate_needq_room, simulate_needq_ret_work_face, simulate_needq_other, # ── 设备信息 ── query_device_info, query_device_type_info, query_monitor_params, # ── 故障诊断 ── check_model_connect_status, check_model_one_dir_cycle, check_model_one_dir_node, check_model_diagonal_structure, get_model_fault_diagnosis, # ── 避灾路线 ── get_escape_path, get_escape_path_each_exit, # ── 关键阻力 / 压能 ── get_out_shafts, get_in_shafts, get_max_resistance_path, get_three_area_distribution, get_key_path_decision, get_path_press_power, # ── 网络解算 ── net_cal, net_cal_for_plan, # ── 报警 / 日志 ── get_alarm_log_history, get_device_set_log_history, get_sys_log_history, # ── 场景管理 ── get_manage_system_by_strType, query_system_by_systemID, # ── 煤矿基础 ── get_gas_identify_vo, get_by_mine_name, query_control_testWind, # ── 数据库 / 文件 ── execute_sql_query, get_file_list_by_type, get_file_base64_by_id, # ── 报表 ── get_latest_report, # ── 基础工具 ── write_todos, # ── 辅助计算工具 ── calc_effective_area, calc_total_air_volume, # 采煤工作面计算 calc_face_by_gas, calc_face_by_workers, calc_face_by_wind_speed, calc_face_air_volume_max, # 掘进工作面计算 calc_tunnel_by_gas, calc_tunnel_by_explosives, calc_tunnel_by_workers, calc_tunnel_by_wind_speed, calc_tunnel_air_volume_max, # 硐室计算 calc_chamber_by_equipment, calc_chamber_by_wind_speed, # 其他巷道计算 calc_other_by_wind_speed, # ── 用户偏好记忆 ── save_user_preference, list_user_preferences, delete_user_preference, # ── 计划审批(Human-in-the-Loop)── request_plan_approval, # ── 联网搜索 ── web_search, web_fetch, # ── 文件内容读取 ── read_file_content, # ── 聊天记录查询 ── search_chat_history, get_session_chat, get_current_session_id, # ── 报告保存 ── save_report, # ── 系统工具 ── get_current_time, ], skills=skills, system_prompt=_dated_prompt, backend=FilesystemBackend(root_dir=PROJECT_ROOT, virtual_mode=True), permissions=[ FilesystemPermission(operations=["write"], paths=["/**"], mode="deny"), ], middleware=[], name="dialog-interpret-agent", checkpointer=MemorySaver(), ) # ── 初始化上下文用量追踪 ── _init_context_tracker_for_dialog() return agent def _init_context_tracker_for_dialog(): """为对话 agent 初始化系统组件 token 预估(统一版:含对话工具 + 计算工具 + MCP)。""" from tools.context_tracker import init_system_components, _tool_to_text _all_tools = [ # 数据查询工具 query_tun_data_by_id, query_device_data, query_device_data_by_id, query_devices_by_tunnel, query_devices_by_tunnel_id, query_devices_by_model, query_tunnel_list, query_tunnels_by_model, get_tun_list_by_modelid, list_ventanaly_monitor_data_days, query_knowledge_base, get_device_kind_dict, get_device_list_by_kind, get_dict_list_by_dictcode, query_device_realtime_data, # 需风量 / 模型 get_model_param_pub_list, get_model_wind, get_sensor_wind, simulate_needq_heading_face, simulate_needq_room, simulate_needq_ret_work_face, simulate_needq_other, # 设备信息 query_device_info, query_device_type_info, query_monitor_params, # 故障诊断 check_model_connect_status, check_model_one_dir_cycle, check_model_one_dir_node, check_model_diagonal_structure, get_model_fault_diagnosis, # 避灾路线 get_escape_path, get_escape_path_each_exit, # 关键阻力 / 压能 get_out_shafts, get_in_shafts, get_max_resistance_path, get_three_area_distribution, get_key_path_decision, get_path_press_power, # 网络解算 net_cal, net_cal_for_plan, # 报警 / 日志 get_alarm_log_history, get_device_set_log_history, get_sys_log_history, # 场景管理 get_manage_system_by_strType, query_system_by_systemID, # 煤矿基础 get_gas_identify_vo, get_by_mine_name, query_control_testWind, # 数据库 / 文件 execute_sql_query, get_file_list_by_type, get_file_base64_by_id, # 报表 get_latest_report, # 基础 + 计算 write_todos, calc_effective_area, calc_total_air_volume, calc_face_by_gas, calc_face_by_workers, calc_face_by_wind_speed, calc_face_air_volume_max, calc_tunnel_by_gas, calc_tunnel_by_explosives, calc_tunnel_by_workers, calc_tunnel_by_wind_speed, calc_tunnel_air_volume_max, calc_chamber_by_equipment, calc_chamber_by_wind_speed, calc_other_by_wind_speed, # 用户偏好 save_user_preference, list_user_preferences, delete_user_preference, # 计划审批 request_plan_approval, # 联网搜索 / 文件读取 / 聊天记录 / 系统 web_search, web_fetch, read_file_content, search_chat_history, get_session_chat, get_current_session_id, save_report, get_current_time, ] tool_texts = [_tool_to_text(t) for t in _all_tools] # MCP 工具单独归类 mcp_text = _tool_to_text(get_needq_all_data) # 读取技能文件内容(动态扫描 skills/ 下所有技能,不再硬编码列表) skill_contents = [] skills_root = Path(__file__).parent.parent / "skills" if skills_root.is_dir(): for skill_dir in sorted(skills_root.iterdir()): if not skill_dir.is_dir() or skill_dir.name.startswith(".") or skill_dir.name == "__pycache__": continue sf = skill_dir / "SKILL.md" if sf.exists(): skill_contents.append(sf.read_text(encoding="utf-8")) print(f"[context_tracker] 已加载技能: {skill_dir.name}") init_system_components( system_prompt=DIALOG_INTERPRET_SYSTEM_PROMPT, tool_defs=tool_texts, skill_contents=skill_contents, mcp_defs=[mcp_text], ) # ============================================================ # 单例缓存 # ============================================================ # 全局 Agent 实例(懒加载) _click_agent: Optional[object] = None _dialog_agent: Optional[object] = None def invalidate_dialog_cache(): """失效对话解读 Agent 缓存(模型切换时调用)。 仅清除 dialog_agent 的缓存,不影响 click_agent(后者继续使用 .env 固定模型)。 """ global _dialog_agent _dialog_agent = None print("[模型切换] dialog-interpret-agent 缓存已失效") def get_click_agent(): """获取点选解读 Agent 单例""" global _click_agent if _click_agent is None: _click_agent = create_click_interpret_agent() return _click_agent def get_dialog_agent(): """获取对话解读 Agent 单例""" global _dialog_agent if _dialog_agent is None: _dialog_agent = create_dialog_interpret_agent() return _dialog_agent