import asyncio from typing import List, Dict, TYPE_CHECKING if TYPE_CHECKING: from core.connection import ConnectionHandler from ..base import IntentProviderBase from plugins_func.functions.play_music import initialize_music_handler from config.logger import setup_logging from core.utils.util import get_system_error_response import re import json import hashlib import time TAG = __name__ logger = setup_logging() class IntentProvider(IntentProviderBase): def __init__(self, config): super().__init__(config) self.llm = None self.promot = "" # 导入全局缓存管理器 from core.utils.cache.manager import cache_manager, CacheType self.cache_manager = cache_manager self.CacheType = CacheType self.history_count = 4 # 默认使用最近4条对话记录 def get_intent_system_prompt(self, functions_list: str) -> str: """ 根据配置的意图选项和可用函数动态生成系统提示词 Args: functions: 可用的函数列表,JSON格式字符串 Returns: 格式化后的系统提示词 """ # 构建函数说明部分 functions_desc = "可用的函数列表:\n" for func in functions_list: func_info = func.get("function", {}) name = func_info.get("name", "") desc = func_info.get("description", "") params = func_info.get("parameters", {}) functions_desc += f"\n函数名: {name}\n" functions_desc += f"描述: {desc}\n" if params: functions_desc += "参数:\n" for param_name, param_info in params.get("properties", {}).items(): param_desc = param_info.get("description", "") param_type = param_info.get("type", "") functions_desc += f"- {param_name} ({param_type}): {param_desc}\n" functions_desc += "---\n" prompt = ( "【严格格式要求】你必须只能返回JSON格式,绝对不能返回任何自然语言!\n\n" "你是一个意图识别助手。请分析用户的最后一句话,判断用户意图并调用相应的函数。\n\n" "【重要规则】以下类型的查询请直接返回result_for_context,无需调用函数:\n" "- 询问当前时间(如:现在几点、当前时间、查询时间等)\n" "- 询问今天日期(如:今天几号、今天星期几、今天是什么日期等)\n" "- 询问今天农历(如:今天农历几号、今天什么节气等)\n" "- 询问所在城市(如:我现在在哪里、你知道我在哪个城市吗等)" "系统会根据上下文信息直接构建回答。\n\n" "- 如果用户使用疑问词(如'怎么'、'为什么'、'如何')询问退出相关的问题(例如'怎么退出了?'),注意这不是让你退出,请返回 {'function_call': {'name': 'continue_chat'}\n" "- 仅当用户明确使用'退出系统'、'结束对话'、'我不想和你说话了'等指令时,才触发 handle_exit_intent\n\n" f"{functions_desc}\n" "处理步骤:\n" "1. 分析用户输入,确定用户意图\n" "2. 检查是否为上述基础信息查询(时间、日期等),如是则返回result_for_context\n" "3. 从可用函数列表中选择最匹配的函数\n" "4. 如果找到匹配的函数,生成对应的function_call 格式\n" '5. 如果没有找到匹配的函数,返回{"function_call": {"name": "continue_chat"}}\n\n' "返回格式要求:\n" "1. 必须返回纯JSON格式,不要包含任何其他文字\n" "2. 必须包含function_call字段\n" "3. function_call必须包含name字段\n" "4. 如果函数需要参数,必须包含arguments字段\n\n" "示例:\n" "```\n" "用户: 现在几点了?\n" '返回: {"function_call": {"name": "result_for_context"}}\n' "```\n" "```\n" "用户: 当前电池电量是多少?\n" '返回: {"function_call": {"name": "get_battery_level", "arguments": {"response_success": "当前电池电量为{value}%", "response_failure": "无法获取Battery的当前电量百分比"}}}\n' "```\n" "```\n" "用户: 当前屏幕亮度是多少?\n" '返回: {"function_call": {"name": "self_screen_get_brightness"}}\n' "```\n" "```\n" "用户: 设置屏幕亮度为50%\n" '返回: {"function_call": {"name": "self_screen_set_brightness", "arguments": {"brightness": 50}}}\n' "```\n" "```\n" "用户: 我想结束对话\n" '返回: {"function_call": {"name": "handle_exit_intent", "arguments": {"say_goodbye": "goodbye"}}}\n' "```\n" "```\n" "用户: 你好啊\n" '返回: {"function_call": {"name": "continue_chat"}}\n' "```\n\n" "注意:\n" "1. 只返回JSON格式,不要包含任何其他文字\n" '2. 优先检查用户查询是否为基础信息(时间、日期等),如是则返回{"function_call": {"name": "result_for_context"}},不需要arguments参数\n' '3. 如果没有找到匹配的函数,返回{"function_call": {"name": "continue_chat"}}\n' "4. 确保返回的JSON格式正确,包含所有必要的字段\n" "5. result_for_context不需要任何参数,系统会自动从上下文获取信息\n" "特殊说明:\n" "- 当用户单次输入包含多个指令时(如'打开灯并且调高音量')\n" "- 请返回多个function_call组成的JSON数组\n" "- 示例:{'function_calls': [{name:'light_on'}, {name:'volume_up'}]}\n\n" "【最终警告】绝对禁止输出任何自然语言、表情符号或解释文字!只能输出有效JSON格式!违反此规则将导致系统错误!" ) return prompt async def replyResult(self, text: str, original_text: str): """使用 asyncio.to_thread 避免阻塞事件循环""" try: user_prompt = ( "请根据以上内容,像人类一样说话的口吻回复用户,要求简洁,请直接返回结果。用户现在说:" + original_text ) # 使用 to_thread 将同步阻塞调用放到线程池中执行,不阻塞事件循环 llm_result = await asyncio.to_thread( self.llm.response_no_stream, system_prompt=text, user_prompt=user_prompt, ) return llm_result except Exception as e: logger.bind(tag=TAG).error(f"Error in generating reply result: {e}") return get_system_error_response(self.config) async def detect_intent( self, conn: "ConnectionHandler", dialogue_history: List[Dict], text: str ) -> str: if not self.llm: raise ValueError("LLM provider not set") if conn.func_handler is None: return '{"function_call": {"name": "continue_chat"}}' # 记录整体开始时间 total_start_time = time.time() # 打印使用的模型信息 model_info = getattr(self.llm, "model_name", str(self.llm.__class__.__name__)) logger.bind(tag=TAG).debug(f"使用意图识别模型: {model_info}") # 计算缓存键 cache_key = hashlib.md5((conn.device_id + text).encode()).hexdigest() # 检查缓存 cached_intent = self.cache_manager.get(self.CacheType.INTENT, cache_key) if cached_intent is not None: cache_time = time.time() - total_start_time logger.bind(tag=TAG).debug( f"使用缓存的意图: {cache_key} -> {cached_intent}, 耗时: {cache_time:.4f}秒" ) return cached_intent if self.promot == "": functions = conn.func_handler.get_functions() if hasattr(conn, "mcp_client"): mcp_tools = conn.mcp_client.get_available_tools() if mcp_tools is not None and len(mcp_tools) > 0: if functions is None: functions = [] functions.extend(mcp_tools) self.promot = self.get_intent_system_prompt(functions) music_config = initialize_music_handler(conn) music_file_names = music_config["music_file_names"] prompt_music = f"{self.promot}\n{music_file_names}\n" home_assistant_cfg = conn.config["plugins"].get("home_assistant") if home_assistant_cfg: devices = home_assistant_cfg.get("devices", []) else: devices = [] if len(devices) > 0: hass_prompt = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n" for device in devices: hass_prompt += device + "\n" prompt_music += hass_prompt logger.bind(tag=TAG).debug(f"User prompt: {prompt_music}") # 构建用户对话历史的提示 msgStr = "" # 获取最近的对话历史 start_idx = max(0, len(dialogue_history) - self.history_count) for i in range(start_idx, len(dialogue_history)): msgStr += f"{dialogue_history[i].role}: {dialogue_history[i].content}\n" msgStr += f"User: {text}\n" user_prompt = f"current dialogue:\n{msgStr}" # 记录预处理完成时间 preprocess_time = time.time() - total_start_time logger.bind(tag=TAG).debug(f"意图识别预处理耗时: {preprocess_time:.4f}秒") # 使用LLM进行意图识别 llm_start_time = time.time() logger.bind(tag=TAG).debug(f"开始LLM意图识别调用, 模型: {model_info}") try: # 使用 to_thread 将同步阻塞调用放到线程池中,避免阻塞事件循环 intent = await asyncio.to_thread( self.llm.response_no_stream, system_prompt=prompt_music, user_prompt=user_prompt, ) except Exception as e: logger.bind(tag=TAG).error(f"Error in intent detection LLM call: {e}") return '{"function_call": {"name": "continue_chat"}}' # 记录LLM调用完成时间 llm_time = time.time() - llm_start_time logger.bind(tag=TAG).debug( f"外挂的大模型意图识别完成, 模型: {model_info}, 调用耗时: {llm_time:.4f}秒" ) # 记录后处理开始时间 postprocess_start_time = time.time() # 清理和解析响应 intent = intent.strip() # 尝试提取JSON部分 match = re.search(r"\{.*\}", intent, re.DOTALL) if match: intent = match.group(0) # 记录总处理时间 total_time = time.time() - total_start_time logger.bind(tag=TAG).debug( f"【意图识别性能】模型: {model_info}, 总耗时: {total_time:.4f}秒, LLM调用: {llm_time:.4f}秒, 查询: '{text[:20]}...'" ) # 尝试解析为JSON try: intent_data = json.loads(intent) # 如果包含function_call,则格式化为适合处理的格式 if "function_call" in intent_data: function_data = intent_data["function_call"] function_name = function_data.get("name") function_args = function_data.get("arguments", {}) # 记录识别到的function call logger.bind(tag=TAG).info( f"llm 识别到意图: {function_name}, 参数: {function_args}" ) # 处理不同类型的意图 if function_name == "result_for_context": # 处理基础信息查询,直接从context构建结果 logger.bind(tag=TAG).info( "检测到result_for_context意图,将使用上下文信息直接回答" ) elif function_name == "continue_chat": # 处理普通对话 # 保留非工具相关的消息 clean_history = [ msg for msg in conn.dialogue.dialogue if msg.role not in ["tool", "function"] ] conn.dialogue.dialogue = clean_history else: # 处理函数调用 logger.bind(tag=TAG).info(f"检测到函数调用意图: {function_name}") # 统一缓存处理和返回 self.cache_manager.set(self.CacheType.INTENT, cache_key, intent) postprocess_time = time.time() - postprocess_start_time logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}秒") return intent except json.JSONDecodeError: # 后处理时间 postprocess_time = time.time() - postprocess_start_time logger.bind(tag=TAG).error( f"无法解析意图JSON: {intent}, 后处理耗时: {postprocess_time:.4f}秒" ) # 如果解析失败,默认返回继续聊天意图 return '{"function_call": {"name": "continue_chat"}}'