diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 6e2b0c37..9221b471 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -49,14 +49,6 @@ prompt: | 你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。 请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。 现在我正在和你进行语音聊天,我们开始吧。 - 如果用户希望结束对话,请在最后说“拜拜”或“再见”。 - #-- hass 相关function的配置格式 请将你家需要控制的设备以下列格式追加在prompt里-- - # 下面是我家智能设备,可以通过homeassistant控制 - # 房间,设备别名,设备id - # 书房,吸顶灯,light.649e3159aa36_light - # 书房,homepod,media_player.shu_fang - # 书房,灯带,light.plug_158df955a6167a - # 书房,窗帘,cover.curtain_158d000710f507 # 使用完声音文件后删除文件(Delete the sound file when you are done using it) delete_audio: true @@ -107,10 +99,12 @@ Intent: - change_role - get_weather - get_news + # play_music是服务器自带的音乐播放,hass_play_music是通过home assistant控制的独立外部程序音乐播放 + # 如果用了hass_play_music,就不要开启play_music,两者只留一个 - play_music - #- hass_play_music #- hass_get_state #- hass_set_state + #- hass_play_music # 插件的基础配置 plugins: @@ -127,6 +121,13 @@ plugins: society: "https://www.chinanews.com.cn/rss/society.xml" world: "https://www.chinanews.com.cn/rss/world.xml" finance: "https://www.chinanews.com.cn/rss/finance.xml" + home_assistant: + devices: + - 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1 + - 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1 + base_url: http://homeassistant.local:8123 + api_key: 你的home assistant api访问令牌 + Memory: mem0ai: @@ -253,12 +254,6 @@ LLM: model_name: deepseek-r1-distill-llama-8b@q4_k_m # 使用的模型名称,需要预先在社区下载 url: http://localhost:1234/v1 # LM Studio服务地址 api_key: lm-studio # LM Studio服务的固定API Key - HomeAssistant: - # 定义LLM API类型 - type: homeassistant - base_url: http://homeassistant.local:8123 - agent_id: conversation.chatgpt - api_key: 你的home assistant api访问令牌 FastgptLLM: # 定义LLM API类型 type: fastgpt @@ -512,20 +507,3 @@ module_test: - "你好,请介绍一下你自己" - "What's the weather like today?" - "请用100字概括量子计算的基本原理和应用前景" - -# 本地音乐播放配置 -music: - music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件 - music_ext: # 音乐文件类型,p3格式效率最高 - - ".mp3" - - ".wav" - - ".p3" - refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒 - -# 以下配置在小于等于0.0.9版本中的docker容器中可用 -# 0.0.9以后的新版本源码部署已经无法奏效 -manager: - enabled: false - ip: 0.0.0.0 - port: 8002 -use_private_config: false diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 718398c3..c0db5eb8 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -196,7 +196,7 @@ class ConnectionHandler: self.prompt = self.prompt + f"\n我在:{self.client_ip_info}" self.dialogue.put(Message(role="system", content=self.prompt)) - self.func_handler = FunctionHandler(self.config) + self.func_handler = FunctionHandler(self) def change_system_prompt(self, prompt): self.prompt = prompt diff --git a/main/xiaozhi-server/core/handle/functionHandler.py b/main/xiaozhi-server/core/handle/functionHandler.py index 3b6d2646..e8056517 100644 --- a/main/xiaozhi-server/core/handle/functionHandler.py +++ b/main/xiaozhi-server/core/handle/functionHandler.py @@ -1,16 +1,16 @@ -import asyncio -from enum import Enum - from config.logger import setup_logging import json from plugins_func.register import FunctionRegistry, ActionResponse, Action, ToolType +from plugins_func.functions.hass_init import append_devices_to_prompt + TAG = __name__ logger = setup_logging() class FunctionHandler: - def __init__(self, config): - self.config = config + def __init__(self, conn): + self.conn = conn + self.config = conn.config self.function_registry = FunctionRegistry() self.register_nessary_functions() self.register_config_functions() @@ -26,9 +26,10 @@ class FunctionHandler: func_names = ",".join(surport_plugins) for function_desc in self.functions_desc: if function_desc["function"]["name"] == "plugin_loader": - function_desc["function"]["description"] = function_desc["function"]["description"].replace("[plugins]", func_names) + function_desc["function"]["description"] = function_desc["function"]["description"].replace("[plugins]", + func_names) break - + def upload_functions_desc(self): self.functions_desc = self.function_registry.get_all_function_desc() @@ -47,7 +48,6 @@ class FunctionHandler: def register_nessary_functions(self): """注册必要的函数""" self.function_registry.register_function("handle_exit_intent") - #self.function_registry.register_function("play_music") self.function_registry.register_function("plugin_loader") self.function_registry.register_function("get_time") self.function_registry.register_function("raise_and_lower_the_volume") @@ -57,6 +57,9 @@ class FunctionHandler: for func in self.config["Intent"]["function_call"].get("functions", []): self.function_registry.register_function(func) + """home assistant需要初始化提示词""" + append_devices_to_prompt(self.conn) + def get_function(self, name): return self.function_registry.get_function(name) diff --git a/main/xiaozhi-server/core/providers/llm/homeassistant/homeassistant.py b/main/xiaozhi-server/core/providers/llm/homeassistant/homeassistant.py deleted file mode 100644 index 115e85e9..00000000 --- a/main/xiaozhi-server/core/providers/llm/homeassistant/homeassistant.py +++ /dev/null @@ -1,62 +0,0 @@ -import requests -from requests.exceptions import RequestException -from config.logger import setup_logging -from core.providers.llm.base import LLMProviderBase - -TAG = __name__ -logger = setup_logging() - - -class LLMProvider(LLMProviderBase): - def __init__(self, config): - self.agent_id = config.get("agent_id") # 对应 agent_id - self.api_key = config.get("api_key") - self.base_url = config.get("base_url", config.get("url")) # 默认使用 base_url - self.api_url = f"{self.base_url}/api/conversation/process" # 拼接完整的 API URL - - def response(self, session_id, dialogue): - print(dialogue) - try: - # home assistant语音助手自带意图,无需使用xiaozhi ai自带的,只需要把用户说的话传递给home assistant即可 - - # 提取最后一个 role 为 'user' 的 content - input_text = None - if isinstance(dialogue, list): # 确保 dialogue 是一个列表 - # 逆序遍历,找到最后一个 role 为 'user' 的消息 - for message in reversed(dialogue): - if message.get("role") == "user": # 找到 role 为 'user' 的消息 - input_text = message.get("content", "") - break # 找到后立即退出循环 - - # 构造请求数据 - payload = { - "text": input_text, - "agent_id": self.agent_id, - "conversation_id": session_id # 使用 session_id 作为 conversation_id - } - # 设置请求头 - headers = { - "Authorization": f"Bearer {self.api_key}", - "Content-Type": "application/json" - } - - # 发起 POST 请求 - response = requests.post(self.api_url, json=payload, headers=headers) - - # 检查请求是否成功 - response.raise_for_status() - - # 解析返回数据 - data = response.json() - speech = data.get("response", {}).get("speech", {}).get("plain", {}).get("speech", "") - - # 返回生成的内容 - if speech: - yield speech - else: - logger.bind(tag=TAG).warning("API 返回数据中没有 speech 内容") - - except RequestException as e: - logger.bind(tag=TAG).error(f"HTTP 请求错误: {e}") - except Exception as e: - logger.bind(tag=TAG).error(f"生成响应时出错: {e}") diff --git a/main/xiaozhi-server/core/utils/dialogue.py b/main/xiaozhi-server/core/utils/dialogue.py index 8d2c161b..d4e66bb6 100644 --- a/main/xiaozhi-server/core/utils/dialogue.py +++ b/main/xiaozhi-server/core/utils/dialogue.py @@ -35,6 +35,15 @@ class Dialogue: self.getMessages(m, dialogue) return dialogue + def update_system_message(self, new_content: str): + """更新或添加系统消息""" + # 查找第一个系统消息 + system_msg = next((msg for msg in self.dialogue if msg.role == "system"), None) + if system_msg: + system_msg.content = new_content + else: + self.put(Message(role="system", content=new_content)) + def get_llm_dialogue_with_memory(self, memory_str: str = None) -> List[Dict[str, str]]: if memory_str is None or len(memory_str) == 0: return self.get_llm_dialogue() diff --git a/main/xiaozhi-server/plugins_func/functions/hass_get_state.py b/main/xiaozhi-server/plugins_func/functions/hass_get_state.py index 34634622..415f35c6 100644 --- a/main/xiaozhi-server/plugins_func/functions/hass_get_state.py +++ b/main/xiaozhi-server/plugins_func/functions/hass_get_state.py @@ -1,4 +1,5 @@ -from plugins_func.register import register_function,ToolType, ActionResponse, Action +from plugins_func.register import register_function, ToolType, ActionResponse, Action +from plugins_func.functions.hass_init import initialize_hass_handler from config.logger import setup_logging import asyncio import requests @@ -6,53 +7,41 @@ import requests TAG = __name__ logger = setup_logging() -HASS_CACHE = {} - -hass_get_state_function_desc ={ - "type": "function", - "function": { - "name": "hass_get_state", - "description": "获取homeassistant里设备的状态,包括灯光亮度,媒体播放器的音量,设备的暂停、继续操作", - "parameters": { - "type": "object", - "properties": { - "entity_id": { - "type": "string", - "description": "需要操作的设备id,homeassistant里的entity_id" - } - }, - "required": ["entity_id"] - } +hass_get_state_function_desc = { + "type": "function", + "function": { + "name": "hass_get_state", + "description": "获取homeassistant里设备的状态,包括灯光亮度,媒体播放器的音量,设备的暂停、继续操作", + "parameters": { + "type": "object", + "properties": { + "entity_id": { + "type": "string", + "description": "需要操作的设备id,homeassistant里的entity_id" } - } - + }, + "required": ["entity_id"] + } + } +} @register_function("hass_get_state", hass_get_state_function_desc, ToolType.SYSTEM_CTL) def hass_get_state(conn, entity_id=''): try: - future = asyncio.run_coroutine_threadsafe( - handle_hass_get_state(conn, entity_id), - conn.loop - ) - ha_response = future.result() - return ActionResponse(action=Action.REQLLM, result="执行成功", response=ha_response) + future = asyncio.run_coroutine_threadsafe( + handle_hass_get_state(conn, entity_id), + conn.loop + ) + ha_response = future.result() + return ActionResponse(action=Action.REQLLM, result="执行成功", response=ha_response) except Exception as e: - logger.bind(tag=TAG).error(f"处理设置属性意图错误: {e}") -def initialize_hass_handler(conn): - config = conn.config - global HASS_CACHE - if HASS_CACHE == {}: - logger.bind(tag=TAG).info(f"实例化HASS:") - if "HomeAssistant" in config["LLM"]: - HASS_CACHE['base_url'] = config["LLM"]['HomeAssistant']['base_url'] - HASS_CACHE['api_key'] = config["LLM"]['HomeAssistant']['api_key'] - else: - logger.bind(tag=TAG).error(f"使用前请在config文件中配置: LLM.HomeAssistant.base_url LLM.HomeAssistant.api_key") + logger.bind(tag=TAG).error(f"处理设置属性意图错误: {e}") + + async def handle_hass_get_state(conn, entity_id): - initialize_hass_handler(conn) - global HASS_CACHE + HASS_CACHE = initialize_hass_handler(conn) api_key = HASS_CACHE['api_key'] base_url = HASS_CACHE['base_url'] url = f"{base_url}/api/states/{entity_id}" @@ -61,10 +50,7 @@ async def handle_hass_get_state(conn, entity_id): "Content-Type": "application/json" } response = requests.get(url, headers=headers) - #logger.bind(tag=TAG).info(f"获取状态: url:{url},return_code:{response.status_code},{response.json()}") if response.status_code == 200: return response.json()['state'] - #return response.json()['attributes'] - #response.attributes else: return f"切换失败,错误码: {response.status_code}" diff --git a/main/xiaozhi-server/plugins_func/functions/hass_play_music.py b/main/xiaozhi-server/plugins_func/functions/hass_play_music.py index 1a4fe74f..502f784e 100644 --- a/main/xiaozhi-server/plugins_func/functions/hass_play_music.py +++ b/main/xiaozhi-server/plugins_func/functions/hass_play_music.py @@ -1,4 +1,5 @@ -from plugins_func.register import register_function,ToolType, ActionResponse, Action +from plugins_func.register import register_function, ToolType, ActionResponse, Action +from plugins_func.functions.hass_init import initialize_hass_handler from config.logger import setup_logging import asyncio import requests @@ -6,43 +7,32 @@ import requests TAG = __name__ logger = setup_logging() -HASS_CACHE = {} - hass_play_music_function_desc = { - "type": "function", - "function": { - "name": "hass_play_music", - "description": "用户想听音乐、有声书的时候使用,在房间的媒体播放器(media_player)里播放对应音频", - "parameters": { - "type": "object", - "properties": { - "media_content_id": { - "type": "string", - "description": "可以是音乐或有声书的专辑名称、歌曲名、演唱者,如果未指定就填random" - }, - "entity_id": { - "type": "string", - "description": "需要操作的音箱的设备id,homeassistant里的entity_id,media_player开头" - } - }, - "required": ["media_content_id", "entity_id"] - } + "type": "function", + "function": { + "name": "hass_play_music", + "description": "用户想听音乐、有声书的时候使用,在房间的媒体播放器(media_player)里播放对应音频", + "parameters": { + "type": "object", + "properties": { + "media_content_id": { + "type": "string", + "description": "可以是音乐或有声书的专辑名称、歌曲名、演唱者,如果未指定就填random" + }, + "entity_id": { + "type": "string", + "description": "需要操作的音箱的设备id,homeassistant里的entity_id,media_player开头" } - } + }, + "required": ["media_content_id", "entity_id"] + } + } +} @register_function('hass_play_music', hass_play_music_function_desc, ToolType.SYSTEM_CTL) - def hass_play_music(conn, entity_id='', media_content_id='random'): try: - #logger.bind(tag=TAG).error(f"arguments: {arguments}") - - #entity_id = arguments["entity_id"] - #media_content_id = arguments["media_content_id"] - - #logger.bind(tag=TAG).error(f"entity_id: {entity_id}") - - # 执行音乐播放命令 future = asyncio.run_coroutine_threadsafe( handle_hass_play_music(conn, entity_id, media_content_id), @@ -53,20 +43,9 @@ def hass_play_music(conn, entity_id='', media_content_id='random'): except Exception as e: logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}") -def initialize_hass_handler(conn): - config = conn.config - global HASS_CACHE - if HASS_CACHE == {}: - logger.bind(tag=TAG).info(f"实例化HASS:") - if "HomeAssistant" in config["LLM"]: - HASS_CACHE['base_url'] = config["LLM"]['HomeAssistant']['base_url'] - HASS_CACHE['api_key'] = config["LLM"]['HomeAssistant']['api_key'] - else: - logger.bind(tag=TAG).error(f"使用前请在config文件中配置: LLM.HomeAssistant.base_url LLM.HomeAssistant.api_key") -async def handle_hass_play_music( conn, entity_id, media_content_id): - initialize_hass_handler(conn) - global HASS_CACHE +async def handle_hass_play_music(conn, entity_id, media_content_id): + HASS_CACHE = initialize_hass_handler(conn) api_key = HASS_CACHE['api_key'] base_url = HASS_CACHE['base_url'] url = f"{base_url}/api/services/music_assistant/play_media" diff --git a/main/xiaozhi-server/plugins_func/functions/hass_set_state.py b/main/xiaozhi-server/plugins_func/functions/hass_set_state.py index da5d1a28..5e2b91a2 100644 --- a/main/xiaozhi-server/plugins_func/functions/hass_set_state.py +++ b/main/xiaozhi-server/plugins_func/functions/hass_set_state.py @@ -1,76 +1,64 @@ -from plugins_func.register import register_function,ToolType, ActionResponse, Action +from plugins_func.register import register_function, ToolType, ActionResponse, Action +from plugins_func.functions.hass_init import initialize_hass_handler from config.logger import setup_logging import asyncio import requests + TAG = __name__ logger = setup_logging() -HASS_CACHE = {} - -hass_set_state_function_desc ={ - "type": "function", - "function": { - "name": "hass_set_state", - "description": "设置homeassistant里设备的状态,包括开、关,调整灯光亮度,调整播放器的音量,设备的暂停、继续、静音操作", - "parameters": { - "type": "object", - "properties": { - "state": { - "type": "object", - "properties": { - "type":{ - "type":"string", - "description":"需要操作的动作,打开设备:turn_on,关闭设备:turn_off,增加亮度:brightness_up,降低亮度:brightness_down,设置亮度:brightness_value,增加>音量:,volume_up降低音量:volume_down,设置音量:volume_set,设备暂停:pause,设备继续:continue,静音/取消静音:volume_mute" - }, - "input":{ - "type":"int", - "description": "只有在设置音量,设置亮度时候才需要,有效值为1-100,对应音量和亮度的1%-100%" - }, - "is_muted":{ - "type":"string", - "description": "只有在设置静音操作时才需要,设置静音的时候该值为true,取消静音时该值为false" - } - }, - "required": ["type"] - }, - "entity_id": { +hass_set_state_function_desc = { + "type": "function", + "function": { + "name": "hass_set_state", + "description": "设置homeassistant里设备的状态,包括开、关,调整灯光亮度,调整播放器的音量,设备的暂停、继续、静音操作", + "parameters": { + "type": "object", + "properties": { + "state": { + "type": "object", + "properties": { + "type": { "type": "string", - "description": "需要操作的设备id,homeassistant里的entity_id" - } + "description": "需要操作的动作,打开设备:turn_on,关闭设备:turn_off,增加亮度:brightness_up,降低亮度:brightness_down,设置亮度:brightness_value,增加>音量:,volume_up降低音量:volume_down,设置音量:volume_set,设备暂停:pause,设备继续:continue,静音/取消静音:volume_mute" }, - "required": ["state", "entity_id"] - } + "input": { + "type": "int", + "description": "只有在设置音量,设置亮度时候才需要,有效值为1-100,对应音量和亮度的1%-100%" + }, + "is_muted": { + "type": "string", + "description": "只有在设置静音操作时才需要,设置静音的时候该值为true,取消静音时该值为false" + } + }, + "required": ["type"] + }, + "entity_id": { + "type": "string", + "description": "需要操作的设备id,homeassistant里的entity_id" } - } - + }, + "required": ["state", "entity_id"] + } + } +} @register_function('hass_set_state', hass_set_state_function_desc, ToolType.SYSTEM_CTL) def hass_set_state(conn, entity_id='', state={}): try: - - future = asyncio.run_coroutine_threadsafe( - handle_hass_set_state(conn, entity_id, state), - conn.loop - ) - ha_response = future.result() - return ActionResponse(action=Action.REQLLM, result="执行成功", response=ha_response) + future = asyncio.run_coroutine_threadsafe( + handle_hass_set_state(conn, entity_id, state), + conn.loop + ) + ha_response = future.result() + return ActionResponse(action=Action.REQLLM, result="执行成功", response=ha_response) except Exception as e: - logger.bind(tag=TAG).error(f"处理设置属性意图错误: {e}") -def initialize_hass_handler(conn): - config = conn.config - global HASS_CACHE - if HASS_CACHE == {}: - logger.bind(tag=TAG).info(f"实例化HASS:") - if "HomeAssistant" in config["LLM"]: - HASS_CACHE['base_url'] = config["LLM"]['HomeAssistant']['base_url'] - HASS_CACHE['api_key'] = config["LLM"]['HomeAssistant']['api_key'] - else: - logger.bind(tag=TAG).error(f"使用前请在config文件中配置: LLM.HomeAssistant.base_url LLM.HomeAssistant.api_key") + logger.bind(tag=TAG).error(f"处理设置属性意图错误: {e}") + async def handle_hass_set_state(conn, entity_id, state): - initialize_hass_handler(conn) - global HASS_CACHE + HASS_CACHE = initialize_hass_handler(conn) api_key = HASS_CACHE['api_key'] base_url = HASS_CACHE['base_url'] ''' @@ -92,7 +80,6 @@ async def handle_hass_set_state(conn, entity_id, state): action = "start" else: action = "turn_on" - action = 'turn_on' elif state['type'] == 'turn_off': description = "设备已关闭" if domain == 'cover': @@ -114,7 +101,7 @@ async def handle_hass_set_state(conn, entity_id, state): elif state['type'] == 'brightness_value': description = f"亮度已调整到{state['input']}" action = 'turn_on' - arg = 'brightness_pct' + arg = 'brightness_pct' value = state['input'] elif state['type'] == 'volume_up': description = "音量已调大" @@ -150,7 +137,7 @@ async def handle_hass_set_state(conn, entity_id, state): else: return f"{domain} {state.type}功能尚未支持" - if arg == '': + if arg == '': data = { "entity_id": entity_id, } @@ -163,11 +150,10 @@ async def handle_hass_set_state(conn, entity_id, state): headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" - } + } response = requests.post(url, headers=headers, json=data) logger.bind(tag=TAG).info(f"设置状态:url:{url},return_code:{response.status_code}") if response.status_code == 200: return description else: return f"设置失败,错误码: {response.status_code}" -