from typing import Any from langchain_core.messages import AIMessage, ToolMessage from langchain_openai import ChatOpenAI from langgraph.checkpoint.memory import MemorySaver from langgraph.prebuilt import create_react_agent import logging import sys import os # 添加父目录到路径以导入config_loader sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from config_loader import get_config_loader from tools import get_ac_status, set_ac_power, set_ac_temperature memory = MemorySaver() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class AirConditionerAgent: SUPPORTED_CONTENT_TYPES = ['text', 'text/plain'] # 默认系统提示词(备用) DEFAULT_SYSTEM_PROMPT = ( '你是一个专门的家庭智能设备管理助手。' '你的主要功能包括:1)控制空调系统;2)查询和管理用户的米家智能设备。' '\n\n## 设备查询功能' '当用户询问"我有哪些设备"、"列出设备"、"设备列表"等问题时,必须调用 list_devices 工具获取所有米家设备信息。' '**重要**: 调用 list_devices 时必须传入 system_user_id 参数,当前用户为 admin,系统ID为 1000000001。' '该工具会自动从数据库读取用户的米家账户凭证,无需用户输入账号密码。' '如果工具返回"未查询到绑定米家账户的Token",请友好地告知用户需要先绑定米家账户。' '如果工具返回"请先开启设备查询MCP",请告知用户MCP服务未启动。' '\n\n## 空调控制功能' '你可以帮助调节温度、设置模式(制冷、制热、送风等)、打开或关闭空调,以及提供节能建议。' '当用户请求查询设备状态时,一定要调用工具 get_ac_status 获取最新状态,并将结果直接返回给用户;如工具返回 JSON,请原样返回或提取关键字段用中文概述。' '当用户请求"启动/打开/关闭空调"等同义表达时,必须调用 set_ac_power(power: bool) 工具执行,并向用户反馈执行结果。' '当用户请求设置温度(如"调到26度/设置到23℃")时,必须调用 set_ac_temperature(temperature: int) 工具执行;如用户未给出明确温度,先向用户确认目标温度(范围16-30℃)。' '\n\n## 智能温度调节' '当用户以语义描述温感(如"有点热/太热/冷一点/暖一点/舒服点/睡觉用")而未给出具体温度时,按以下规则自动设置人类适宜温度:' '1) 先调用 get_ac_status 获取当前 power、mode、tar_temp;若电源关闭且需要调温,先调用 set_ac_power(true)。' '2) 若 mode 为 制冷/自动 且用户表达"有点热/太热/降温/冷一点",将目标温度在当前基础上降低1-2℃(默认2℃),不低于24℃;若表达"有点冷/太冷/升温/暖一点",则提高1-2℃(默认2℃),不高于30℃,然后调用 set_ac_temperature。' '3) 若 mode 为 制热 且用户表达"有点冷/太冷/升温/暖一点",在当前基础上提高1-2℃(默认2℃),不高于26℃;若表达"有点热/太热/降温/冷一点",则降低1-2℃(默认2℃),不低于16℃,然后调用 set_ac_temperature。' '4) 若用户表达"舒适/舒服点",则:制冷模式设为26℃,制热模式设为22℃;若无法判断模式,则先查询状态后按模式执行。' '5) 若用户表达"睡觉/睡眠",则:制冷模式设为27℃,制热模式设为21℃。' '所有自动推断出的目标温度都必须限制在16-30℃区间内。设置完成后,用中文简要说明采用了哪条规则与最终温度。' '\n\n如果用户询问与智能设备管理或空调控制无关的内容,请礼貌地说明你只能协助处理智能设备相关的问题。' ) def __init__(self): # 从数据库加载配置(严格模式:配置加载失败则退出) try: config_loader = get_config_loader(strict_mode=True) # 加载AI模型配置 ai_config = config_loader.get_default_ai_model_config() logger.info(f"✅ 成功加载AI模型配置: {ai_config['model']}") self.model = ChatOpenAI( model=ai_config['model'], api_key=ai_config['api_key'], base_url=ai_config['api_base'], temperature=ai_config['temperature'], ) # 加载系统提示词 try: system_prompt = config_loader.get_agent_prompt('air_conditioner') self.SYSTEM_PROMPT = system_prompt except Exception as e: self.SYSTEM_PROMPT = self.DEFAULT_SYSTEM_PROMPT except Exception as e: logger.error(f"❌ 配置加载失败: {e}") logger.error("⚠️ 请确保:") logger.error(" 1. StarRocks 数据库已启动") logger.error(" 2. 已执行数据库初始化脚本: data/init_config.sql 和 data/ai_config.sql") logger.error(" 3. config.yaml 中的数据库连接配置正确") raise SystemExit(1) from e from tools import list_devices self.tools = [get_ac_status, set_ac_power, set_ac_temperature, list_devices] self.graph = create_react_agent( self.model, tools=self.tools, checkpointer=memory, prompt=self.SYSTEM_PROMPT, ) async def invoke(self, query, context_id) -> dict[str, Any]: """非流式调用,直接返回最终结果""" inputs = {'messages': [('user', query)]} config = {'configurable': {'thread_id': context_id}} # 直接调用invoke,不使用stream result = self.graph.invoke(inputs, config) return self.get_agent_response(config) def _extract_text_from_message(self, msg: AIMessage | ToolMessage | Any) -> str: try: content = getattr(msg, 'content', None) if isinstance(content, str): return content if isinstance(content, list): parts = [] for part in content: if isinstance(part, dict) and 'text' in part: parts.append(part['text']) if parts: return '\n'.join(parts) except Exception: pass return '' def get_agent_response(self, config): current_state = self.graph.get_state(config) messages = current_state.values.get('messages') if hasattr(current_state, 'values') else None # 优先返回最近一次工具消息内容 if isinstance(messages, list) and messages: for msg in reversed(messages): if isinstance(msg, ToolMessage): tool_text = self._extract_text_from_message(msg) if tool_text: return { 'is_task_complete': True, 'require_user_input': False, 'content': tool_text, } # 回退到最后一条 AI 消息 final_text = '' if isinstance(messages, list) and messages: last_msg = messages[-1] final_text = self._extract_text_from_message(last_msg) if not final_text: return { 'is_task_complete': False, 'require_user_input': True, 'content': '当前无法处理您的请求,请稍后重试。', } return { 'is_task_complete': True, 'require_user_input': False, 'content': final_text, }