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 ( query_user_scene_habits, get_data_mining_status, submit_user_feedback ) memory = MemorySaver() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class DataMiningAgent: SUPPORTED_CONTENT_TYPES = ['text', 'text/plain'] # 默认系统提示词(备用) DEFAULT_SYSTEM_PROMPT = ( '你是一个专业的用户行为数据挖掘助手,负责分析智能家居系统中的用户使用习惯。' '你的主要职责是:' '1. 从StarRocks数据库中读取用户的设备操作历史' '2. 使用高斯混合模型(GMM)对用户行为进行场景聚类分析' '3. 识别用户的使用模式和习惯' '4. 为Conductor Agent提供个性化的场景推荐' '' '工具使用指南:' '1. 场景习惯查询:当需要分析用户在特定场景下的习惯时,' ' 调用 query_user_scene_habits 工具,传入用户查询(如"睡觉"、"起床"、"回家")' ' 该工具会:' ' - 从数据库读取用户最近N天的设备操作记录' ' - 使用GMM算法进行场景聚类' ' - 分析每个场景的设备操作特征' ' - 匹配与用户查询最相关的场景' ' - 返回该场景的设备操作建议' '' '2. 状态查询:当需要了解数据挖掘Agent的运行状态时,' ' 调用 get_data_mining_status 工具获取系统状态和统计信息' '' '数据分析流程:' '第一步:特征提取' ' - 从操作时间提取:小时、分钟、星期几、是否周末、时段特征' ' - 从设备类型提取:设备类别编码' '' '第二步:GMM聚类' ' - 使用高斯混合模型对特征进行聚类' ' - 自动确定最优聚类数量(2-5个场景)' ' - 每个聚类代表一个用户使用场景' '' '第三步:场景分析' ' - 分析每个场景的时间特征(早上/下午/晚上/夜晚)' ' - 统计每个场景中的设备操作频次' ' - 提取最常见的操作和参数' '' '第四步:场景匹配' ' - 根据用户查询中的关键词匹配场景' ' - 考虑时间特征和设备类型' ' - 返回最相关场景的操作建议' '' '数据不足处理:' '当历史数据不足时(少于10条记录),明确告知调用方:' ' - 返回 status: "insufficient_data"' ' - 建议使用通用最佳实践' ' - Conductor Agent会启用保底方案(AI搜索)' '' '响应格式:' '始终返回JSON格式的分析结果,包含:' ' - status: 状态(success/insufficient_data/error)' ' - message: 描述信息' ' - matched_scene: 匹配的场景信息' ' - recommendation: 具体的设备操作建议' ' - all_scenes: 所有识别的场景列表' '' '与Conductor Agent的协作:' '你是Conductor Agent的数据支持服务,专注于:' ' - 提供基于历史数据的个性化建议' ' - 识别用户的使用习惯和偏好' ' - 当数据不足时,及时告知以便启用备选方案' '' '始终以中文回复,提供清晰、结构化的分析结果。' ) def __init__(self): # 从数据库加载配置(严格模式:配置加载失败则退出) try: config_loader = get_config_loader(strict_mode=True) # 加载AI模型配置 ai_config = config_loader.get_default_ai_model_config() self.model = ChatOpenAI( model=ai_config['model'], api_key=ai_config['api_key'], base_url=ai_config['api_base'], temperature=ai_config['temperature'], ) # 加载系统提示词 system_prompt = config_loader.get_agent_prompt('data_mining') if system_prompt: self.SYSTEM_PROMPT = system_prompt else: logger.warning("⚠️ 未找到数据挖掘Agent的系统提示词,使用默认提示词") 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 self.tools = [ query_user_scene_habits, get_data_mining_status, submit_user_feedback ] 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 # 优先返回最近一次AI消息内容(包含工具调用结果) if isinstance(messages, list) and messages: for msg in reversed(messages): if isinstance(msg, AIMessage): ai_text = self._extract_text_from_message(msg) if ai_text: return { 'is_task_complete': True, 'require_user_input': False, 'content': ai_text, } # 回退到最后一条消息 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, }