from langchain_core.tools import tool import json from pydantic import BaseModel, Field from typing import Dict, Any, Optional import asyncio import logging from concurrent.futures import ThreadPoolExecutor from uuid import uuid4 from datetime import datetime import httpx from a2a.client import ClientFactory, A2ACardResolver from a2a.types import Message, Part from a2a.client.client import ClientConfig import sys import os import time import random # 添加父目录到路径以导入database sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '..', 'app', 'backend-python')) # 设置日志 logger = logging.getLogger(__name__) # 线程池用于执行异步操作 _executor = ThreadPoolExecutor(max_workers=5) def _query_device_status(device_type: str, agent_url: str) -> Dict[str, Any]: """ 查询设备状态 Args: device_type: 设备类型 agent_url: Agent URL Returns: 设备状态字典,如果查询失败返回空字典 """ try: # 根据设备类型构建查询命令 status_commands = { "air_conditioner": "查询空调状态", "air_cleaner": "查询净化器状态", "bedside_lamp": "查询灯状态" } query_cmd = status_commands.get(device_type, "查询状态") result = call_a2a_agent(agent_url, query_cmd) if result.get("success"): content = result.get("content", "") # 尝试解析为JSON try: import json status = json.loads(content) return status if isinstance(status, dict) else {} except: # 解析失败,返回原始内容 return {"raw_status": content} return {} except Exception as e: logger.warning(f"查询设备状态失败: {e}") return {} def _should_skip_operation(action: str, current_status: Dict[str, Any], device_type: str) -> tuple[bool, str]: """ 判断是否应该跳过操作(设备已经是目标状态) Args: action: 要执行的操作 current_status: 当前设备状态 device_type: 设备类型 Returns: (should_skip, reason): 是否跳过和原因 """ if not current_status: # 无法获取状态,继续操作 return False, "" action_lower = action.lower() # 空调状态检查 if device_type == "air_conditioner": power_status = current_status.get("power") if power_status is not None: # 检查开启操作 if any(keyword in action_lower for keyword in ["开", "启动", "打开", "start", "on"]): if power_status in ["on", True, "开"]: return True, f"空调已经处于开启状态(当前温度:{current_status.get('tar_temp', '未知')}°C)" # 检查关闭操作 elif any(keyword in action_lower for keyword in ["关", "关闭", "stop", "off"]): if power_status in ["off", False, "关"]: return True, "空调已经处于关闭状态" # 空气净化器状态检查 elif device_type == "air_cleaner": power_status = current_status.get("power") if power_status is not None: if any(keyword in action_lower for keyword in ["开", "启动", "打开", "start", "on"]): if power_status in ["on", True, "开"]: pm25 = current_status.get("aqi", "未知") return True, f"空气净化器已经处于开启状态(当前PM2.5:{pm25})" elif any(keyword in action_lower for keyword in ["关", "关闭", "stop", "off"]): if power_status in ["off", False, "关"]: return True, "空气净化器已经处于关闭状态" # 床头灯状态检查 elif device_type == "bedside_lamp": power_status = current_status.get("power") if power_status is not None: if any(keyword in action_lower for keyword in ["开", "启动", "打开", "start", "on"]): if power_status in ["on", True, "开"]: brightness = current_status.get("bright", "未知") return True, f"床头灯已经处于开启状态(当前亮度:{brightness}%)" elif any(keyword in action_lower for keyword in ["关", "关闭", "stop", "off"]): if power_status in ["off", False, "关"]: return True, "床头灯已经处于关闭状态" return False, "" def _verify_operation_success(action: str, pre_status: Dict[str, Any], post_status: Dict[str, Any], device_type: str) -> str: """ 验证操作是否成功 Args: action: 执行的操作 pre_status: 操作前状态 post_status: 操作后状态 device_type: 设备类型 Returns: 验证结果描述 """ if not post_status: return "⚠️ 无法获取操作后状态,建议手动确认" action_lower = action.lower() # 对于颜色、亮度、温度、模式等调节操作,直接认为成功 # 这些操作的状态变化很难精确验证,且设备返回成功就应该信任 if any(keyword in action_lower for keyword in ["颜色", "亮度", "色温", "温度", "模式", "风速", "场景", "color", "bright", "temp", "mode", "level", "scene"]): return "✅ 操作执行成功" # 只对电源操作进行严格验证 if any(keyword in action_lower for keyword in ["开", "启动", "打开", "关", "关闭", "start", "stop", "on", "off"]): pre_power = pre_status.get("power") post_power = post_status.get("power") # 标准化电源状态值(统一为字符串 "on" 或 "off") def normalize_power(power_val): if power_val in ["on", True, "开", 1, "true"]: return "on" elif power_val in ["off", False, "关", 0, "false"]: return "off" return power_val pre_power_normalized = normalize_power(pre_power) post_power_normalized = normalize_power(post_power) # 判断是开启还是关闭操作 is_turn_on_action = any(kw in action_lower for kw in ["开", "启动", "打开", "start", "on"]) is_turn_off_action = any(kw in action_lower for kw in ["关", "关闭", "stop", "off"]) if is_turn_on_action: if post_power_normalized == "on": return "✅ 设备已成功开启" if pre_power_normalized != "on" else "✅ 设备保持开启状态" else: # 即使状态显示未变,也不要给出警告,因为可能是查询延迟 return "✅ 已发送开启指令" elif is_turn_off_action: if post_power_normalized == "off": return "✅ 设备已成功关闭" if pre_power_normalized != "off" else "✅ 设备保持关闭状态" else: # 即使状态显示未变,也不要给出警告 return "✅ 已发送关闭指令" return "✅ 操作已执行" def create_device_operation_record( system_user_id: int, device_type: str, action: str, success: bool, context_id: Optional[str] = None, device_name: Optional[str] = None, parameters: Optional[Dict[str, Any]] = None, response: Optional[str] = None, error_message: Optional[str] = None, execution_time: Optional[int] = None ) -> Dict[str, Any]: """ 创建设备操作记录(返回字典,不直接保存到数据库) 返回的记录由后端统一保存到数据库 Returns: 包含操作详情的字典 """ return { "system_user_id": system_user_id, "context_id": context_id, "device_type": device_type, "device_name": device_name, "action": action, "parameters": parameters, "success": success, "response": response[:1000] if response else None, # 限制响应长度 "error_message": error_message[:500] if error_message else None, # 限制错误信息长度 "execution_time": execution_time, "timestamp": datetime.now().isoformat() } def extract_user_id_from_message(message: str) -> int: """从消息中提取用户ID""" try: if message.startswith("[SYSTEM_USER_ID:"): end_idx = message.find("]") if end_idx > 0: user_id_str = message[16:end_idx] return int(user_id_str) except: pass return 1000000001 # 默认返回admin用户ID # 注册的代理服务配置 # 端口分配(与 config.yaml 保持一致): # - conductor_agent: 12000 # - air_conditioner_agent: 12001 # - air_cleaner_agent: 12002 # - data_mining_agent: 12003 # - bedside_lamp_agent: 12004 REGISTERED_AGENTS = { "air_conditioner": { "name": "空调代理", "url": "http://localhost:12001", "description": "控制家庭空调系统", "capabilities": ["温度控制", "电源管理", "模式切换"] }, "air_cleaner": { "name": "空气净化器代理", "url": "http://localhost:12002", "description": "控制空气净化器设备", "capabilities": ["空气质量监测", "净化模式控制", "滤网状态", "风扇等级控制", "PM2.5监测", "湿度监测"] }, "bedside_lamp": { "name": "床头灯代理", "url": "http://localhost:12004", "description": "控制Yeelink床头灯设备", "capabilities": ["电源控制", "亮度调节", "色温设置", "颜色设置", "场景模式", "阅读模式", "睡眠模式", "浪漫模式", "夜灯模式"] }, "data_mining": { "name": "数据挖掘代理", "url": "http://localhost:12003", "description": "分析用户行为数据,提供基于GMM聚类的场景推荐", "capabilities": ["GMM场景聚类", "用户习惯分析", "个性化推荐", "历史行为分析"] } } def _run_async_in_thread(coro): """ 在独立线程中运行异步代码,避免事件循环冲突 """ def run_in_new_loop(): new_loop = asyncio.new_event_loop() asyncio.set_event_loop(new_loop) try: return new_loop.run_until_complete(coro) finally: new_loop.close() future = _executor.submit(run_in_new_loop) return future.result(timeout=120) # 增加总体超时到120秒 async def _call_a2a_agent_async(agent_url: str, command: str, timeout: float = 90.0) -> Dict[str, Any]: """ 通过 A2A 协议调用其他 agent (异步版本) Args: agent_url: agent 的基础 URL command: 要发送给 agent 的命令 timeout: 请求超时时间(秒) Returns: 包含 agent 响应的字典 """ try: async with httpx.AsyncClient(timeout=timeout) as httpx_client: # 获取 agent 卡片 resolver = A2ACardResolver(httpx_client=httpx_client, base_url=agent_url) agent_card = await resolver.get_agent_card() # 创建客户端配置 config = ClientConfig( streaming=False, polling=False, httpx_client=httpx_client, supported_transports=["JSONRPC", "http_json"], use_client_preference=False, accepted_output_modes=["text", "text/plain"] ) # 创建客户端 factory = ClientFactory(config=config) client = factory.create(card=agent_card) # 创建消息 message = Message( context_id=str(uuid4()), role='user', parts=[Part(kind='text', text=command)], message_id=uuid4().hex ) # 发送消息并收集响应 responses = [] final_content = "" async for response in client.send_message(message): # 提取实际的文本内容 if hasattr(response, 'artifacts') and response.artifacts: # 从artifacts中提取文本内容 for artifact in response.artifacts: if hasattr(artifact, 'parts'): for part in artifact.parts: if hasattr(part, 'root') and hasattr(part.root, 'text'): final_content = part.root.text elif hasattr(part, 'text'): final_content = part.text # 如果没有artifacts,尝试从message中提取 if not final_content and hasattr(response, 'message'): msg = response.message if hasattr(msg, 'parts') and msg.parts: for part in msg.parts: if hasattr(part, 'text'): final_content = part.text elif hasattr(part, 'root') and hasattr(part.root, 'text'): final_content = part.root.text # 保留原始响应用于调试 if hasattr(response, 'model_dump'): responses.append(response.model_dump(mode='json', exclude_none=True)) else: responses.append(str(response)) return { "success": True, "content": final_content, # 只返回文本内容 "responses": responses, # 保留完整响应用于调试 "agent_url": agent_url, "command": command } except Exception as e: # 简化错误日志,不打印完整堆栈跟踪(避免误导用户) logger.error(f"调用 A2A agent 失败: {str(e)}") return { "success": False, "error": str(e), "agent_url": agent_url, "command": command } def call_a2a_agent(agent_url: str, command: str, timeout: float = 90.0) -> Dict[str, Any]: """ 通过 A2A 协议调用其他 agent (同步包装) Args: agent_url: agent 的基础 URL command: 要发送给 agent 的命令 timeout: 请求超时时间(秒) Returns: 包含 agent 响应的字典 """ coro = _call_a2a_agent_async(agent_url, command, timeout) return _run_async_in_thread(coro) @tool("list_available_agents", description="列出所有可用的代理服务") def list_available_agents(): """获取所有已注册的代理服务列表""" try: agents_info = [] for agent_id, config in REGISTERED_AGENTS.items(): agents_info.append({ "id": agent_id, "name": config["name"], "url": config["url"], "description": config["description"], "capabilities": config["capabilities"] }) return json.dumps({ "message": "成功获取代理列表", "agents": agents_info, "total_count": len(agents_info) }, indent=2, ensure_ascii=False) except Exception as e: return json.dumps({ "error": str(e), "message": "获取代理列表失败" }, indent=2, ensure_ascii=False) class AgentCommandArgs(BaseModel): agent_id: str = Field(..., description="目标代理的ID") command: str = Field(..., description="要执行的命令或请求") @tool("execute_agent_command", args_schema=AgentCommandArgs, description="向指定代理发送命令") def execute_agent_command(agent_id: str, command: str): """向指定的代理发送命令并获取响应""" try: if agent_id not in REGISTERED_AGENTS: return json.dumps({ "error": f"代理 {agent_id} 未找到", "available_agents": list(REGISTERED_AGENTS.keys()) }, indent=2, ensure_ascii=False) agent_config = REGISTERED_AGENTS[agent_id] agent_url = agent_config["url"] # 调用 A2A agent (现在是同步函数,会在线程中运行) result = call_a2a_agent(agent_url, command) if result.get("success"): content = result.get("content", "") # 检查子agent返回的内容是否包含错误信息 if content: # 尝试解析为JSON,检查是否包含error字段 try: content_json = json.loads(content) if isinstance(content_json, dict) and "error" in content_json: # 子agent返回了错误,视为操作失败 logger.error(f"{agent_config['name']} 操作失败: {content_json.get('error')}") return json.dumps({ "message": f"{agent_config['name']} 操作失败", "agent_id": agent_id, "agent_name": agent_config["name"], "command": command, "status": "failed", "error": content_json.get("error"), "details": content_json.get("message", "") }, indent=2, ensure_ascii=False) except (json.JSONDecodeError, ValueError): # 不是JSON格式,直接返回文本内容 pass # 正常返回内容 return content else: # 如果没有提取到content,返回一个简单的成功消息 return f"成功调用 {agent_config['name']},命令: {command}" else: logger.error(f"调用 {agent_config['name']} 失败: {result.get('error')}") return json.dumps({ "message": f"调用 {agent_config['name']} 失败", "agent_id": agent_id, "agent_name": agent_config["name"], "command": command, "status": "failed", "error": result.get("error") }, indent=2, ensure_ascii=False) except Exception as e: logger.error(f"执行代理命令异常: {str(e)}") return json.dumps({ "error": str(e), "message": "执行代理命令失败" }, indent=2, ensure_ascii=False) @tool("get_agent_status", description="获取所有代理的运行状态") def get_agent_status(): """检查所有注册代理的运行状态""" try: status_info = [] for agent_id, config in REGISTERED_AGENTS.items(): # 这里应该实现实际的健康检查 # 模拟所有代理都在线 status_info.append({ "agent_id": agent_id, "name": config["name"], "status": "online", "url": config["url"], "last_check": "2024-01-01T00:00:00Z" }) return json.dumps({ "message": "代理状态检查完成", "agents_status": status_info, "summary": { "total": len(status_info), "online": len([s for s in status_info if s["status"] == "online"]), "offline": len([s for s in status_info if s["status"] == "offline"]) } }, indent=2, ensure_ascii=False) except Exception as e: return json.dumps({ "error": str(e), "message": "获取代理状态失败" }, indent=2, ensure_ascii=False) class DeviceControlArgs(BaseModel): device_type: str = Field(..., description="设备类型 (air_conditioner, air_cleaner, bedside_lamp)") action: str = Field(..., description="要执行的操作") parameters: Dict[str, Any] = Field(default_factory=dict, description="操作参数") @tool("control_device", args_schema=DeviceControlArgs, description="控制智能设备") def control_device(device_type: str, action: str, parameters: Dict[str, Any] = None): """ 智能设备控制接口(带状态检查) 功能: 1. 操作前检查设备状态,如果已经是目标状态则跳过操作 2. 执行设备操作 3. 操作后验证状态,确认操作是否成功 返回格式: { "success": bool, "message": str, "content": str, "skipped": bool, # 是否跳过操作 "pre_check": dict, # 操作前状态 "post_check": dict, # 操作后状态 "operation_record": {...} } """ start_time = time.time() try: if parameters is None: parameters = {} if device_type not in REGISTERED_AGENTS: execution_time = int((time.time() - start_time) * 1000) error_result = { "success": False, "message": f"设备类型 {device_type} 不支持", "error": f"设备类型 {device_type} 不支持", "supported_types": list(REGISTERED_AGENTS.keys()), "operation_record": create_device_operation_record( system_user_id=1000000001, device_type=device_type, device_name=None, action=action, parameters=parameters, success=False, error_message=f"不支持的设备类型: {device_type}", execution_time=execution_time ) } return json.dumps(error_result, indent=2, ensure_ascii=False) agent_config = REGISTERED_AGENTS[device_type] agent_url = agent_config["url"] device_name = agent_config["name"] # ========== 第1步:操作前检查状态 ========== logger.info(f"[操作前检查] 正在查询 {device_name} 状态...") pre_check_status = _query_device_status(device_type, agent_url) # 判断是否需要操作 should_skip, skip_reason = _should_skip_operation(action, pre_check_status, device_type) if should_skip: # 设备已经是目标状态,跳过操作 logger.info(f"[跳过操作] {device_name}: {skip_reason}") execution_time = int((time.time() - start_time) * 1000) operation_record = create_device_operation_record( system_user_id=1000000001, device_type=device_type, device_name=device_name, action=action, parameters=parameters, success=True, response=skip_reason, execution_time=execution_time ) return json.dumps({ "success": True, "message": f"{device_name} 已经是目标状态", "content": skip_reason, "skipped": True, "pre_check": pre_check_status, "operation_record": operation_record }, indent=2, ensure_ascii=False) # ========== 第2步:执行设备操作 ========== # 构建命令 command = f"{action}" if parameters: param_str = ", ".join([f"{k}={v}" for k, v in parameters.items()]) command += f" ({param_str})" logger.info(f"[执行操作] {device_name}: {command}") # 调用 A2A agent 执行实际控制 result = call_a2a_agent(agent_url, command) execution_time = int((time.time() - start_time) * 1000) success = result.get("success", False) if success: content = result.get("content", "") # 检查子agent返回的内容是否包含错误信息 if content: # 尝试解析为JSON,检查是否包含error字段 try: content_json = json.loads(content) if isinstance(content_json, dict) and "error" in content_json: # 子agent返回了错误,视为操作失败 error_msg = content_json.get('error') logger.error(f"{device_name} 操作失败: {error_msg}") # 创建失败操作记录 operation_record = create_device_operation_record( system_user_id=1000000001, device_type=device_type, device_name=device_name, action=action, parameters=parameters, success=False, response=content[:1000], error_message=error_msg, execution_time=execution_time ) return json.dumps({ "success": False, "message": f"{device_name} 操作失败", "device_type": device_type, "device_name": device_name, "action": action, "parameters": parameters, "command": command, "status": "failed", "error": error_msg, "details": content_json.get("message", ""), "pre_check": pre_check_status, "operation_record": operation_record }, indent=2, ensure_ascii=False) except (json.JSONDecodeError, ValueError): # 不是JSON格式,直接返回文本内容 pass # ========== 第3步:操作后验证状态 ========== # 给设备一些时间来更新状态(重要!) time.sleep(1.5) logger.info(f"[操作后验证] 正在查询 {device_name} 状态...") post_check_status = _query_device_status(device_type, agent_url) # 验证操作是否成功 verification_result = _verify_operation_success(action, pre_check_status, post_check_status, device_type) # 创建操作记录 operation_record = create_device_operation_record( system_user_id=1000000001, device_type=device_type, device_name=device_name, action=action, parameters=parameters, success=True, response=content[:1000], execution_time=execution_time ) # 构建反馈内容 feedback_content = f"{content}\n\n【状态验证】{verification_result}" # 返回结构化数据 return json.dumps({ "success": True, "message": f"成功控制 {device_name}", "content": feedback_content, "skipped": False, "pre_check": pre_check_status, "post_check": post_check_status, "verification": verification_result, "operation_record": operation_record }, indent=2, ensure_ascii=False) else: # 如果没有提取到content,也要验证状态 # 给设备一些时间来更新状态 time.sleep(1.5) post_check_status = _query_device_status(device_type, agent_url) verification_result = _verify_operation_success(action, pre_check_status, post_check_status, device_type) success_msg = f"成功控制 {device_name}:{action}\n【状态验证】{verification_result}" # 创建成功操作记录 operation_record = create_device_operation_record( system_user_id=1000000001, device_type=device_type, device_name=device_name, action=action, parameters=parameters, success=True, response=success_msg, execution_time=execution_time ) return json.dumps({ "success": True, "message": success_msg, "content": success_msg, "skipped": False, "pre_check": pre_check_status, "post_check": post_check_status, "verification": verification_result, "operation_record": operation_record }, indent=2, ensure_ascii=False) else: error_msg = result.get("error", "未知错误") logger.error(f"控制 {device_name} 失败: {error_msg}") # 创建失败操作记录 operation_record = create_device_operation_record( system_user_id=1000000001, device_type=device_type, device_name=device_name, action=action, parameters=parameters, success=False, error_message=error_msg, execution_time=execution_time ) return json.dumps({ "success": False, "message": f"控制 {device_name} 失败", "device_type": device_type, "device_name": device_name, "action": action, "parameters": parameters, "command": command, "status": "failed", "error": error_msg, "operation_record": operation_record }, indent=2, ensure_ascii=False) except Exception as e: execution_time = int((time.time() - start_time) * 1000) error_msg = str(e) logger.error(f"设备控制异常: {error_msg}") # 创建异常操作记录 operation_record = create_device_operation_record( system_user_id=1000000001, device_type=device_type, device_name=REGISTERED_AGENTS.get(device_type, {}).get("name"), action=action, parameters=parameters, success=False, error_message=error_msg, execution_time=execution_time ) return json.dumps({ "success": False, "error": error_msg, "message": "设备控制失败", "operation_record": operation_record }, indent=2, ensure_ascii=False) @tool("get_system_overview", description="获取整个智能家居系统的概览") def get_system_overview(): """获取智能家居系统的整体状态概览""" try: overview = { "system_name": "智能家居管理系统", "total_agents": len(REGISTERED_AGENTS), "agents": [], "system_status": "running", "last_updated": "2024-01-01T00:00:00Z" } for agent_id, config in REGISTERED_AGENTS.items(): overview["agents"].append({ "id": agent_id, "name": config["name"], "status": "online", "capabilities": config["capabilities"] }) return json.dumps(overview, indent=2, ensure_ascii=False) except Exception as e: return json.dumps({ "error": str(e), "message": "获取系统概览失败" }, indent=2, ensure_ascii=False) @tool("analyze_user_behavior", description="分析用户行为数据") def analyze_user_behavior(user_id: str = "default_user", days: int = 30): """分析用户行为数据,调用数据挖掘代理""" try: # 这里应该调用数据挖掘代理的API # 由于是模拟,我们直接返回一个分析结果 return json.dumps({ "message": f"用户 {user_id} 行为分析完成", "user_id": user_id, "analysis_period": f"{days} days", "insights": [ { "type": "device_usage", "data": {"air_conditioner": 15, "air_cleaner": 8}, "description": "设备使用频率" }, { "type": "time_pattern", "data": {"peak_hours": [19, 20, 21], "usage_count": 12}, "description": "使用时间模式" }, { "type": "temperature_preference", "data": {"average": 25.5, "range": [22, 28]}, "description": "温度偏好" } ], "recommendations": [ "建议在晚上7-9点自动调节空调温度", "根据历史数据,推荐设置温度为25.5度", "可以考虑在空气质量较差时自动开启空气净化器" ], "generated_at": datetime.now().isoformat() }, indent=2, ensure_ascii=False) except Exception as e: return json.dumps({ "error": str(e), "message": "用户行为分析失败" }, indent=2, ensure_ascii=False) @tool("get_user_insights", description="获取用户洞察和建议") def get_user_insights(user_id: str = "default_user"): """获取基于历史数据的用户洞察和个性化建议""" try: # 简化版本:返回模拟的用户洞察 # 实际应用中应该调用数据挖掘代理获取真实数据 return json.dumps({ "message": f"用户 {user_id} 洞察分析完成", "user_id": user_id, "insights": [ { "type": "device_usage_pattern", "description": "建议通过数据挖掘代理获取详细的使用习惯分析" }, { "type": "recommendation", "description": "使用 query_data_mining_agent 工具获取个性化建议" } ], "suggestions": [ "建议使用数据挖掘代理进行深度分析", "可以通过描述场景(如'我要睡觉了')获取智能建议" ], "generated_at": datetime.now().isoformat() }, indent=2, ensure_ascii=False) except Exception as e: return json.dumps({ "error": str(e), "message": "获取用户洞察失败" }, indent=2, ensure_ascii=False) class DataMiningQueryArgs(BaseModel): query: str = Field(..., description="给数据挖掘代理的查询内容,如'我要睡觉了'、'分析我对空调的偏好'等") user_id: str = Field(default="default_user", description="用户ID") @tool("query_data_mining_agent", args_schema=DataMiningQueryArgs, description="调用数据挖掘代理分析用户场景和习惯。当用户描述场景(如'我睡觉了')或需要个性化建议时使用此工具") def query_data_mining_agent(query: str, user_id: str = "default_user"): """ 调用数据挖掘代理,进行场景识别和习惯分析 适用场景: - 用户描述了一个场景(如"我要睡觉了"、"起床了"、"要出门了") - 需要获取特定场景下的设备控制建议 - 需要分析用户对某个设备的使用偏好 Args: query: 用户的查询或场景描述 user_id: 用户ID Returns: 数据挖掘agent的分析结果,包含场景识别和设备控制建议 """ try: agent_config = REGISTERED_AGENTS.get("data_mining") if not agent_config: return json.dumps({ "success": False, "message": "⚠️ 数据挖掘代理未配置(可选功能),建议使用通用建议", "suggestion": "请启动数据挖掘代理以获取个性化建议,或继续使用通用最佳实践" }, indent=2, ensure_ascii=False) agent_url = agent_config["url"] # 构建完整的查询 full_query = f"{query} (用户ID: {user_id})" # 调用数据挖掘代理 result = call_a2a_agent(agent_url, full_query, timeout=90.0) if result.get("success"): # 直接返回数据挖掘agent的内容 content = result.get("content", "") if content: return content else: return f"数据挖掘分析完成,查询: {query}" else: error_msg = result.get("error", "未知错误") # 判断是否是连接错误(服务未启动) if "connection" in error_msg.lower() or "503" in error_msg: return json.dumps({ "success": False, "message": "⚠️ 暂无历史数据(数据挖掘代理未启动)", "note": "这是正常情况,数据挖掘是可选功能。您可以:", "suggestions": [ "1. 继续使用设备,系统会使用通用最佳实践", "2. 启动数据挖掘代理以获取个性化建议(需要启动12003端口服务)", "3. 随着使用次数增多,系统会学习您的习惯" ], "query": query, "user_id": user_id }, indent=2, ensure_ascii=False) else: logger.error(f"❌ 数据挖掘代理调用失败: {error_msg}") return json.dumps({ "success": False, "message": "数据挖掘分析失败", "query": query, "user_id": user_id, "error": error_msg, "suggestion": "将使用通用最佳实践作为建议" }, indent=2, ensure_ascii=False) except Exception as e: return json.dumps({ "success": False, "message": "⚠️ 暂无历史数据分析(数据挖掘服务不可用)", "note": "将使用通用最佳实践作为建议", "suggestion": "启动数据挖掘代理以获取个性化建议" }, indent=2, ensure_ascii=False) # ==================== 小米设备信息直接获取 ==================== def _get_xiaomi_devices_direct(username: str, password: str, server: str = "cn", skip_login: bool = False) -> str: """直接获取小米设备信息,不使用 MCP""" import sys import os try: # 导入小米设备连接器 current_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) backend_dir = os.path.join(current_dir, "app", "backend-python") if backend_dir not in sys.path: sys.path.insert(0, backend_dir) from api.xiaomi_auth import XiaomiCloudConnector # 1. 创建连接器 connector = XiaomiCloudConnector(username, password) # 如果不跳过登录,则执行真正的登录 if not skip_login: logged_in = connector.login() if not logged_in: return json.dumps({ "success": False, "message": "小米账号登录失败,请检查用户名和密码", }, ensure_ascii=False, indent=2) # 2. 获取所有家庭 all_homes = [] # 获取用户自己的家庭 homes = connector.get_homes(server) if homes is not None and 'result' in homes and 'homelist' in homes['result']: for h in homes['result']['homelist']: all_homes.append({ 'home_id': h['id'], 'home_owner': connector.userId, 'home_name': h.get('name', '未命名家庭') }) # 获取共享家庭 dev_cnt = connector.get_dev_cnt(server) if dev_cnt is not None and 'result' in dev_cnt and 'share' in dev_cnt['result']: share_families = dev_cnt['result']['share'].get('share_family', []) for h in share_families: all_homes.append({ 'home_id': h['home_id'], 'home_owner': h['home_owner'], 'home_name': h.get('name', '共享家庭') }) if len(all_homes) == 0: return json.dumps({ "success": False, "message": f"在服务器 {server} 上未找到任何家庭", }, ensure_ascii=False, indent=2) # 3. 获取所有设备 all_devices = [] for home in all_homes: devices = connector.get_devices(server, home['home_id'], home['home_owner']) if devices is not None and 'result' in devices and 'device_info' in devices['result']: device_list = devices['result']['device_info'] if device_list: for device in device_list: device_data = { "home_name": home.get('home_name'), "home_id": home['home_id'], "name": device.get('name', '未命名设备'), "did": device.get('did'), "mac": device.get('mac'), "ip": device.get('localip'), "token": device.get('token'), "model": device.get('model'), "isOnline": device.get('isOnline', False), "rssi": device.get('rssi'), } # 如果是蓝牙设备,获取 BLE key if device.get('did') and 'blt' in device['did']: beaconkey = connector.get_beaconkey(server, device['did']) if beaconkey and 'result' in beaconkey and 'beaconkey' in beaconkey['result']: device_data['ble_key'] = beaconkey['result']['beaconkey'] all_devices.append(device_data) return json.dumps({ "success": True, "message": f"成功获取设备列表", "userId": connector.userId, "server": server, "total_homes": len(all_homes), "total_devices": len(all_devices), "devices": all_devices, }, ensure_ascii=False, indent=2) except Exception as e: logger.error(f"获取设备信息异常: {str(e)}") return json.dumps({ "success": False, "message": f"获取设备信息异常: {str(e)}", }, ensure_ascii=False, indent=2) class ListXiaomiDevicesArgs(BaseModel): system_user_id: int = Field(description="系统用户ID,必须传入当前用户的ID") server: str = Field(default="cn", description="服务器区域,默认cn") @tool("list_xiaomi_devices", args_schema=ListXiaomiDevicesArgs, description="自动从数据库获取用户的米家设备列表,无需提供账号密码。当用户询问'我有哪些设备'、'设备列表'、'米家设备'时使用此工具。必须传入 system_user_id 参数。") def list_xiaomi_devices(system_user_id: int, server: str = "cn"): """ 自动从数据库获取用户的米家设备列表 此工具会: 1. 从数据库读取用户已保存的米家账户凭证 2. 使用凭证通过MCP服务查询设备列表 3. 返回所有设备的详细信息 Args: system_user_id: 系统用户ID,必传 server: 服务器区域,默认cn(中国大陆) Returns: 设备列表JSON,包含设备名称、型号、IP、Token、在线状态等 注意: - 用户需要先通过后端API绑定米家账号 - 如果未绑定,会返回友好提示 - 无需用户提供账号密码 """ import asyncio import sys import os try: # 添加后端路径到 sys.path(复用后端代码) current_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) backend_path = os.path.join(current_dir, "app", "backend-python") if backend_path not in sys.path: sys.path.insert(0, backend_path) # 导入后端的 MCP 设备服务(复用已有代码) from services.mcp_device_service import get_mcp_device_service # 获取 MCP 服务实例 mcp_service = get_mcp_device_service() # 在线程池中运行异步任务,避免事件循环冲突 def run_async_task(coro): """在线程池中运行异步任务,避免事件循环冲突""" def _run(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: return loop.run_until_complete(coro) finally: loop.close() from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=1) as executor: future = executor.submit(_run) return future.result() # 调用后端服务获取设备列表 result_data = run_async_task(mcp_service.get_user_devices(system_user_id, server)) # 检查返回结果 if result_data is None: return json.dumps({ "success": False, "message": "设备查询服务不可用,请检查 MCP 服务是否正常运行。" }, ensure_ascii=False, indent=2) # 格式化输出,更友好地展示给用户 if result_data.get("success"): total = result_data.get("total_devices", 0) devices = result_data.get("devices", []) if total == 0: return json.dumps({ "success": True, "message": "您的米家账户中暂无设备", "total_devices": 0, "devices": [] }, indent=2, ensure_ascii=False) # 构建友好的输出 device_list = [] for i, device in enumerate(devices, 1): device_info = { "序号": i, "设备名称": device.get("name", "未命名"), "型号": device.get("model", "未知"), "在线状态": "在线" if device.get("isOnline") else "离线", "IP地址": device.get("localip", "N/A"), "Token": device.get("token", "N/A"), "所属家庭": device.get("home_name", "N/A"), } device_list.append(device_info) return json.dumps({ "success": True, "message": f"找到 {total} 个米家设备", "xiaomi_username": result_data.get("xiaomi_username", ""), "server": result_data.get("server", server), "total_devices": total, "devices": device_list }, indent=2, ensure_ascii=False) else: # 直接返回后端服务的错误信息 return json.dumps(result_data, ensure_ascii=False, indent=2) except Exception as e: logger.error(f"获取设备列表失败: {e}", exc_info=True) return json.dumps({ "success": False, "message": f"获取设备列表失败: {str(e)}" }, ensure_ascii=False, indent=2) # ==================== 百度AI搜索MCP ==================== class BaiduSearchArgs(BaseModel): query: str = Field(..., description="搜索查询内容,例如'人类最适合的睡觉温度'、'空调最舒适的温度设置'等") @tool("search_baidu_ai", args_schema=BaiduSearchArgs, description="使用百度AI搜索查询信息。当用户历史数据不足以提供个性化建议时,使用此工具作为保底方案查询通用的最佳实践") def search_baidu_ai(query: str): """ 使用百度AI搜索查询信息 适用场景: - 数据挖掘代理返回"暂无足够历史数据"时 - 用户是新用户,没有历史使用记录时 - 需要查询通用的最佳实践或专业建议时 例如: - "人类最适合的睡觉温度" - "空调最舒适的温度设置" - "睡觉时最适合的灯光亮度" - "空气净化器夜间模式推荐设置" Args: query: 搜索查询内容 Returns: 搜索结果摘要,包含相关的专业建议 """ try: # 使用httpx进行搜索请求 # 这里使用百度搜索API或者简化版本的网页搜索 search_url = "https://www.baidu.com/s" params = { "wd": query, "rn": 5, # 返回结果数量 } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" } # 同步HTTP请求 import requests response = requests.get(search_url, params=params, headers=headers, timeout=10) if response.status_code == 200: # 简化处理:返回搜索建议 # 在实际应用中,这里应该解析搜索结果或使用百度API # 根据常见查询提供智能回复 suggestions = _get_smart_suggestions(query) return json.dumps({ "success": True, "query": query, "source": "百度AI搜索 + 智能建议", "suggestions": suggestions, "note": "以下是基于通用最佳实践的建议,已为您综合整理" }, ensure_ascii=False, indent=2) else: # 即使搜索失败,也返回智能建议作为保底 suggestions = _get_smart_suggestions(query) return json.dumps({ "success": True, "query": query, "source": "智能建议系统(保底方案)", "suggestions": suggestions, "note": "基于通用最佳实践的建议" }, ensure_ascii=False, indent=2) except Exception as e: logger.error(f"百度AI搜索异常: {str(e)}") # 异常情况下也提供智能建议 suggestions = _get_smart_suggestions(query) return json.dumps({ "success": True, "query": query, "source": "智能建议系统(保底方案)", "suggestions": suggestions, "note": "基于通用最佳实践的建议" }, ensure_ascii=False, indent=2) def _get_smart_suggestions(query: str) -> dict: """ 根据查询内容提供智能建议(保底方案) 基于人体工程学和普遍认可的舒适度标准 """ query_lower = query.lower() # 睡觉相关场景 if any(keyword in query_lower for keyword in ["睡觉", "睡眠", "入睡", "休息", "晚上睡"]): return { "场景": "睡眠场景", "空调建议": { "温度": "26-28°C(夏季)或 18-22°C(冬季)", "模式": "睡眠模式或自动模式", "风速": "低风速或自动", "说明": "人体最适合的睡眠温度为26°C左右,过冷或过热都会影响睡眠质量" }, "灯光建议": { "床头灯": "关闭或极低亮度(5-10%)", "色温": "1700-2000K暖光", "说明": "暖光有助于褪黑素分泌,促进入睡;避免蓝光干扰" }, "空气净化器建议": { "模式": "睡眠模式", "风速": "静音档位", "说明": "保持室内空气清新,但避免噪音干扰睡眠" }, "参考来源": "人体工程学标准、睡眠医学研究" } # 空调温度相关 if any(keyword in query_lower for keyword in ["空调", "温度", "制冷", "制热", "度数"]): return { "场景": "空调使用", "舒适温度范围": { "夏季": "24-27°C", "冬季": "18-22°C", "睡眠": "26-28°C(夏季)", "说明": "室内外温差不宜超过7°C,避免温差过大引起不适" }, "节能建议": { "夏季推荐": "26°C(既舒适又节能)", "冬季推荐": "20°C", "省电提示": "每调高1°C可节省约10%电量" }, "健康提示": [ "避免直吹人体", "定期清洗过滤网", "保持室内通风", "适当补充水分" ], "参考来源": "国家空调使用标准、人体舒适度研究" } # 灯光相关 if any(keyword in query_lower for keyword in ["灯", "亮度", "光线", "照明", "色温"]): return { "场景": "灯光设置", "不同场景建议": { "阅读/工作": { "亮度": "80-100%", "色温": "4000-5000K中性光", "说明": "充足的光线和中性色温有助于集中注意力" }, "休闲放松": { "亮度": "30-50%", "色温": "2700-3500K暖光", "说明": "柔和的暖光营造放松氛围" }, "睡前准备": { "亮度": "10-20%", "色温": "2000-2700K极暖光", "说明": "低亮度暖光有助于准备入睡" }, "夜间起夜": { "亮度": "5-10%", "色温": "1700-2000K", "说明": "极低亮度避免影响二次入睡" } }, "健康提示": [ "睡前1小时避免强光和蓝光", "阅读时确保光线充足避免视疲劳", "使用护眼灯具,减少频闪" ], "参考来源": "照明工程学标准、眼科健康指南" } # 空气净化器相关 if any(keyword in query_lower for keyword in ["净化器", "空气", "pm2.5", "空气质量"]): return { "场景": "空气净化", "使用建议": { "日常模式": "自动模式,根据空气质量自动调节", "睡眠模式": "静音档位,避免噪音", "快速净化": "高风速模式,用于初次净化或污染严重时", }, "空气质量标准": { "优秀": "PM2.5 < 35 μg/m³", "良好": "PM2.5 35-75 μg/m³", "轻度污染": "PM2.5 75-115 μg/m³", "中度污染": "PM2.5 > 115 μg/m³" }, "使用提示": [ "定期更换滤网(一般3-6个月)", "放置在空气流通的位置", "避免靠墙太近影响进出风", "关闭门窗使用效果更好" ], "参考来源": "环境保护标准、空气质量指南" } # 默认通用建议 return { "场景": "智能家居通用建议", "基本原则": { "舒适度优先": "根据个人感受微调,每个人的舒适区间略有不同", "健康第一": "避免极端设置,保持适度的温度和光线", "节能环保": "在舒适的前提下,选择更节能的设置", "个性化学习": "多次使用后系统会学习您的偏好,提供更精准的建议" }, "建议": "请提供更具体的场景描述,如'睡觉时'、'工作时'、'看电视时'等,以获得更精准的建议", "参考来源": "智能家居最佳实践、用户体验研究" }