Merge pull request #3544 from xinnan-tech/py-hass-devices
Py hass devices
This commit is contained in:
3 files changed
+128
-29
No files matched your search
@@ -5,6 +5,7 @@ if TYPE_CHECKING:
|
|||||||
from core.connection import ConnectionHandler
|
from core.connection import ConnectionHandler
|
||||||
from ..base import IntentProviderBase
|
from ..base import IntentProviderBase
|
||||||
from plugins_func.functions.play_music import initialize_music_handler
|
from plugins_func.functions.play_music import initialize_music_handler
|
||||||
|
from plugins_func.functions.hass_init import _devices_text, _hass_plugin_config
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from core.utils.util import get_system_error_response
|
from core.utils.util import get_system_error_response
|
||||||
import re
|
import re
|
||||||
@@ -181,16 +182,11 @@ class IntentProvider(IntentProviderBase):
|
|||||||
music_file_names = music_config["music_file_names"]
|
music_file_names = music_config["music_file_names"]
|
||||||
prompt_music = f"{self.promot}\n<musicNames>{music_file_names}\n</musicNames>"
|
prompt_music = f"{self.promot}\n<musicNames>{music_file_names}\n</musicNames>"
|
||||||
|
|
||||||
home_assistant_cfg = conn.config["plugins"].get("home_assistant")
|
plugin_config = _hass_plugin_config(conn.config.get("plugins", {}))
|
||||||
if home_assistant_cfg:
|
devices_text = _devices_text(plugin_config.get("devices", ""))
|
||||||
devices = home_assistant_cfg.get("devices", [])
|
if devices_text:
|
||||||
else:
|
|
||||||
devices = []
|
|
||||||
if len(devices) > 0:
|
|
||||||
hass_prompt = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n"
|
hass_prompt = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n"
|
||||||
for device in devices:
|
prompt_music += hass_prompt + devices_text + "\n"
|
||||||
hass_prompt += device + "\n"
|
|
||||||
prompt_music += hass_prompt
|
|
||||||
|
|
||||||
logger.bind(tag=TAG).debug(f"User prompt: {prompt_music}")
|
logger.bind(tag=TAG).debug(f"User prompt: {prompt_music}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,53 @@
|
|||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from core.utils.util import check_model_key
|
from core.utils.util import check_model_key
|
||||||
|
from plugins_func.register import module_func_map
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_module_names(funcs):
|
||||||
|
"""把 functions 列表里的模块名(如 hass_state)展开成具体函数名,与工具加载口径一致。"""
|
||||||
|
expanded = []
|
||||||
|
for name in funcs:
|
||||||
|
expanded.extend(module_func_map.get(name, [name]))
|
||||||
|
return expanded
|
||||||
|
|
||||||
|
|
||||||
|
def _devices_text(devices) -> str:
|
||||||
|
"""把 devices 配置转成拼进提示词的文字。
|
||||||
|
|
||||||
|
config.yaml 示例和手机端智控台给的是列表,每项一条"位置,设备名,entity_id",这里拼成每行一条;
|
||||||
|
网页端智控台和 HA 集成文档是一段字符串,原样用;其它(如 None)当空。
|
||||||
|
"""
|
||||||
|
if isinstance(devices, (list, tuple)):
|
||||||
|
return "\n".join(
|
||||||
|
str(d).strip() for d in devices if d is not None and str(d).strip()
|
||||||
|
)
|
||||||
|
return devices if isinstance(devices, str) else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _hass_plugin_config(plugins_config: dict) -> dict:
|
||||||
|
"""取 HA 插件配置:优先本地 config.yaml 的 home_assistant,其次智控台下发的 hass_state。"""
|
||||||
|
for key in ("home_assistant", "hass_state"):
|
||||||
|
cfg = plugins_config.get(key)
|
||||||
|
if cfg:
|
||||||
|
return cfg
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def append_devices_to_prompt(conn):
|
def append_devices_to_prompt(conn):
|
||||||
if conn.intent_type == "function_call":
|
if conn.intent_type == "function_call":
|
||||||
funcs = conn.config["Intent"][conn.config["selected_module"]["Intent"]].get(
|
funcs = _expand_module_names(
|
||||||
"functions", []
|
conn.config["Intent"][conn.config["selected_module"]["Intent"]].get(
|
||||||
)
|
"functions", []
|
||||||
|
)
|
||||||
# 安全地获取插件配置
|
|
||||||
plugins_config = conn.config.get("plugins", {})
|
|
||||||
config_source = (
|
|
||||||
"home_assistant"
|
|
||||||
if plugins_config.get("home_assistant")
|
|
||||||
else "hass_state"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if "hass_get_state" in funcs or "hass_set_state" in funcs:
|
if "hass_get_state" in funcs or "hass_set_state" in funcs:
|
||||||
prompt = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n"
|
prompt = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n"
|
||||||
deviceStr = plugins_config.get(config_source, {}).get("devices", "")
|
plugin_config = _hass_plugin_config(conn.config.get("plugins", {}))
|
||||||
|
deviceStr = _devices_text(plugin_config.get("devices", ""))
|
||||||
conn.prompt += prompt + deviceStr + "\n"
|
conn.prompt += prompt + deviceStr + "\n"
|
||||||
# 更新提示词
|
# 更新提示词
|
||||||
conn.dialogue.update_system_message(conn.prompt)
|
conn.dialogue.update_system_message(conn.prompt)
|
||||||
@@ -32,17 +58,10 @@ def initialize_hass_handler(conn):
|
|||||||
if not conn.load_function_plugin:
|
if not conn.load_function_plugin:
|
||||||
return ha_config
|
return ha_config
|
||||||
|
|
||||||
# 安全地获取插件配置
|
plugin_config = _hass_plugin_config(conn.config.get("plugins", {}))
|
||||||
plugins_config = conn.config.get("plugins", {})
|
if not plugin_config:
|
||||||
# 确定配置来源
|
|
||||||
config_source = (
|
|
||||||
"home_assistant" if plugins_config.get("home_assistant") else "hass_state"
|
|
||||||
)
|
|
||||||
if not plugins_config.get(config_source):
|
|
||||||
return ha_config
|
return ha_config
|
||||||
|
|
||||||
# 统一获取配置
|
|
||||||
plugin_config = plugins_config[config_source]
|
|
||||||
ha_config["base_url"] = plugin_config.get("base_url")
|
ha_config["base_url"] = plugin_config.get("base_url")
|
||||||
ha_config["api_key"] = plugin_config.get("api_key")
|
ha_config["api_key"] = plugin_config.get("api_key")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"""append_devices_to_prompt:devices 写成列表或一段字符串都能拼进提示词。
|
||||||
|
|
||||||
|
config.yaml 示例和手机端智控台给的是列表,以前会报
|
||||||
|
can only concatenate str (not "list") to str,设备清单进不了提示词,模型只能猜 entity_id。
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import plugins_func.functions.hass_state # noqa: F401 注册 hass_state 模块,填充 module_func_map
|
||||||
|
from plugins_func.functions.hass_init import append_devices_to_prompt
|
||||||
|
|
||||||
|
HEADER = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n"
|
||||||
|
BASE_PROMPT = "你是小智。"
|
||||||
|
|
||||||
|
|
||||||
|
class FakeDialogue:
|
||||||
|
def __init__(self):
|
||||||
|
self.system_messages = []
|
||||||
|
|
||||||
|
def update_system_message(self, prompt):
|
||||||
|
self.system_messages.append(prompt)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeConn:
|
||||||
|
def __init__(self, plugin, source="home_assistant", functions=("hass_get_state", "hass_set_state")):
|
||||||
|
self.intent_type = "function_call"
|
||||||
|
self.config = {
|
||||||
|
"selected_module": {"Intent": "function_call"},
|
||||||
|
"Intent": {"function_call": {"functions": list(functions)}},
|
||||||
|
"plugins": {source: plugin},
|
||||||
|
}
|
||||||
|
self.prompt = BASE_PROMPT
|
||||||
|
self.dialogue = FakeDialogue()
|
||||||
|
|
||||||
|
|
||||||
|
def _plugin(**extra):
|
||||||
|
return {"base_url": "http://homeassistant.local:8123", "api_key": "test-token", **extra}
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_devices_one_per_line():
|
||||||
|
conn = FakeConn(_plugin(devices=[
|
||||||
|
"客厅,玩具灯,switch.toy_lamp",
|
||||||
|
" 卧室,台灯,switch.desk_lamp ",
|
||||||
|
"",
|
||||||
|
]))
|
||||||
|
append_devices_to_prompt(conn)
|
||||||
|
expected = BASE_PROMPT + HEADER + "客厅,玩具灯,switch.toy_lamp\n卧室,台灯,switch.desk_lamp\n"
|
||||||
|
assert conn.prompt == expected
|
||||||
|
assert conn.dialogue.system_messages == [expected]
|
||||||
|
|
||||||
|
|
||||||
|
def test_string_devices_kept_as_is():
|
||||||
|
# 网页端智控台和 HA 集成文档给的是一段字符串
|
||||||
|
devices = "客厅,玩具灯,switch.toy_lamp;\n卧室,台灯,switch.desk_lamp;"
|
||||||
|
conn = FakeConn(_plugin(devices=devices))
|
||||||
|
append_devices_to_prompt(conn)
|
||||||
|
assert conn.prompt == BASE_PROMPT + HEADER + devices + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_console_hass_state_list():
|
||||||
|
# 智控台下发的插件配置键是 hass_state;手机端把 array 字段按行切成了列表
|
||||||
|
conn = FakeConn(_plugin(devices=["客厅,玩具灯,switch.toy_lamp"]), source="hass_state")
|
||||||
|
append_devices_to_prompt(conn)
|
||||||
|
assert conn.prompt.endswith(HEADER + "客厅,玩具灯,switch.toy_lamp\n")
|
||||||
|
|
||||||
|
|
||||||
|
def test_module_name_in_functions():
|
||||||
|
# functions 里写模块名 hass_state(config.yaml 注释的写法),展开后应能匹配
|
||||||
|
conn = FakeConn(_plugin(devices=["客厅,玩具灯,switch.toy_lamp"]), functions=("hass_state",))
|
||||||
|
append_devices_to_prompt(conn)
|
||||||
|
assert conn.prompt.endswith(HEADER + "客厅,玩具灯,switch.toy_lamp\n")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("plugin", [_plugin(), _plugin(devices=None), _plugin(devices=[])])
|
||||||
|
def test_missing_or_empty_devices_does_not_raise(plugin):
|
||||||
|
conn = FakeConn(plugin)
|
||||||
|
append_devices_to_prompt(conn)
|
||||||
|
assert conn.prompt == BASE_PROMPT + HEADER + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_without_hass_functions_prompt_unchanged():
|
||||||
|
conn = FakeConn(_plugin(devices=["客厅,玩具灯,switch.toy_lamp"]), functions=("get_weather",))
|
||||||
|
append_devices_to_prompt(conn)
|
||||||
|
assert conn.prompt == BASE_PROMPT
|
||||||
|
assert conn.dialogue.system_messages == []
|
||||||
Reference in new issue
Block a user