diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 6473b08..2a7034a 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -20,33 +20,33 @@ jobs: matrix: include: # ARM variants - - home_assistant_version: "2024.2.1" + - home_assistant_version: "2024.12.3" arch: "aarch64" - - home_assistant_version: "2024.2.1" + - home_assistant_version: "2024.12.3" arch: "armhf" # Base x86 - - home_assistant_version: "2024.2.1" + - home_assistant_version: "2024.12.3" suffix: "-noavx" arch: "amd64" extra_defines: "-DGGML_AVX=OFF -DGGML_AVX2=OFF -DGGML_FMA=OFF -DGGML_F16C=OFF" - - home_assistant_version: "2024.2.1" + - home_assistant_version: "2024.12.3.1" arch: "i386" suffix: "-noavx" extra_defines: "-DGGML_AVX=OFF -DGGML_AVX2=OFF -DGGML_FMA=OFF -DGGML_F16C=OFF" # AVX2 and AVX512 - - home_assistant_version: "2024.2.1" + - home_assistant_version: "2024.12.3" arch: "amd64" extra_defines: "-DGGML_AVX=ON -DGGML_AVX2=ON -DGGML_FMA=ON -DGGML_F16C=ON" - - home_assistant_version: "2024.2.1" + - home_assistant_version: "2024.12.3.1" arch: "amd64" suffix: "-avx512" extra_defines: "-DGGML_AVX512=ON -DGGML_FMA=ON -DGGML_F16C=ON" - - home_assistant_version: "2024.2.1" + - home_assistant_version: "2024.12.3" arch: "i386" extra_defines: "-DGGML_AVX=ON -DGGML_AVX2=ON -DGGML_FMA=ON -DGGML_F16C=ON" - - home_assistant_version: "2024.2.1" + - home_assistant_version: "2024.12.3" arch: "i386" suffix: "-avx512" extra_defines: "-DGGML_AVX512=ON -DGGML_FMA=ON -DGGML_F16C=ON" diff --git a/README.md b/README.md index 855739a..6845751 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This project provides the required "glue" components to control your Home Assist Please see the [Setup Guide](./docs/Setup.md) for more information on installation. ## Local LLM Conversation Integration -**The latest version of this integration requires Home Assistant 2024.8.0 or newer** +**The latest version of this integration requires Home Assistant 2024.12.3 or newer** In order to integrate with Home Assistant, we provide a custom component that exposes the locally running LLM as a "conversation agent". @@ -150,6 +150,7 @@ In order to facilitate running the project entirely on the system where Home Ass ## Version History | Version | Description | |---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| v0.3.7 | Update llama.cpp version to support newer models, Update minimum Home Assistant version to 2024.12.3, Add German In-Context Learning examples, Fix multi-turn use, Fix an issue with webcolors | | v0.3.6 | Small llama.cpp backend fixes | | v0.3.5 | Fix for llama.cpp backend installation, Fix for Home LLM v1-3 API parameters, add Polish ICL examples | | v0.3.4 | Significantly improved language support including full Polish translation, Update bundled llama-cpp-python to support new models, various bug fixes | diff --git a/custom_components/llama_conversation/config_flow.py b/custom_components/llama_conversation/config_flow.py index 0eb7953..cc7e6ea 100644 --- a/custom_components/llama_conversation/config_flow.py +++ b/custom_components/llama_conversation/config_flow.py @@ -37,7 +37,7 @@ from homeassistant.helpers.selector import ( from homeassistant.util.package import is_installed from importlib.metadata import version -from .utils import download_model_from_hf, install_llama_cpp_python, format_url, MissingQuantizationException +from .utils import download_model_from_hf, get_llama_cpp_python_version, install_llama_cpp_python, format_url, MissingQuantizationException from .const import ( CONF_CHAT_MODEL, CONF_MAX_TOKENS, @@ -352,7 +352,9 @@ class ConfigFlow(BaseLlamaConversationConfigFlow, config_entries.ConfigFlow, dom local_backend = is_local_backend(user_input[CONF_BACKEND_TYPE]) self.model_config.update(user_input) if local_backend: - if is_installed("llama-cpp-python") and version("llama-cpp-python") == EMBEDDED_LLAMA_CPP_PYTHON_VERSION: + installed_version = await self.hass.async_add_executor_job(get_llama_cpp_python_version) + _LOGGER.debug(f"installed version: {installed_version}") + if installed_version == EMBEDDED_LLAMA_CPP_PYTHON_VERSION: return await self.async_step_local_model() else: return await self.async_step_install_local_wheels() diff --git a/custom_components/llama_conversation/const.py b/custom_components/llama_conversation/const.py index c2f88f2..d73af54 100644 --- a/custom_components/llama_conversation/const.py +++ b/custom_components/llama_conversation/const.py @@ -383,5 +383,5 @@ OPTIONS_OVERRIDES = { }, } -INTEGRATION_VERSION = "0.3.6" -EMBEDDED_LLAMA_CPP_PYTHON_VERSION = "0.2.88" \ No newline at end of file +INTEGRATION_VERSION = "0.3.7" +EMBEDDED_LLAMA_CPP_PYTHON_VERSION = "0.3.5" \ No newline at end of file diff --git a/custom_components/llama_conversation/conversation.py b/custom_components/llama_conversation/conversation.py index 6458287..6d891b4 100644 --- a/custom_components/llama_conversation/conversation.py +++ b/custom_components/llama_conversation/conversation.py @@ -421,8 +421,10 @@ class LocalLLMAgent(ConversationEntity, AbstractConversationAgent): response=intent_response, conversation_id=conversation_id ) + tool_response = None # parse response to_say = service_call_pattern.sub("", response.strip()) + tool_response = None for block in service_call_pattern.findall(response.strip()): parsed_tool_call: dict = json.loads(block) @@ -505,8 +507,11 @@ class LocalLLMAgent(ConversationEntity, AbstractConversationAgent): ) # handle models that generate a function call and wait for the result before providing a response - if self.entry.options.get(CONF_TOOL_MULTI_TURN_CHAT, DEFAULT_TOOL_MULTI_TURN_CHAT): - conversation.append({"role": "tool", "message": json.dumps(tool_response)}) + if self.entry.options.get(CONF_TOOL_MULTI_TURN_CHAT, DEFAULT_TOOL_MULTI_TURN_CHAT) and tool_response is not None: + try: + conversation.append({"role": "tool", "message": json.dumps(tool_response)}) + except: + conversation.append({"role": "tool", "message": "No tools were used in this response."}) # generate a response based on the tool result try: @@ -527,6 +532,7 @@ class LocalLLMAgent(ConversationEntity, AbstractConversationAgent): ) conversation.append({"role": "assistant", "message": response}) + conversation.append({"role": "assistant", "message": to_say}) # generate intent response to Home Assistant intent_response = intent.IntentResponse(language=user_input.language) @@ -749,12 +755,14 @@ class LocalLLMAgent(ConversationEntity, AbstractConversationAgent): value = attributes[attribute_name] if value is not None: - if attribute_name == "temperature": - value = int(value) - if value > 50: - value = f"{value}F" - else: - value = f"{value}C" + # try to apply unit if present + unit_suffix = attributes.get(f"{attribute_name}_unit") + if unit_suffix: + value = f"{value} {unit_suffix}" + elif attribute_name == "temperature": + # try to get unit or guess otherwise + suffix = "F" if value > 50 else "C" + value = F"{int(value)} {suffix}" elif attribute_name == "rgb_color": value = F"{closest_color(value)} {value}" elif attribute_name == "volume_level": diff --git a/custom_components/llama_conversation/in_context_examples_de.csv b/custom_components/llama_conversation/in_context_examples_de.csv new file mode 100644 index 0000000..5d88950 --- /dev/null +++ b/custom_components/llama_conversation/in_context_examples_de.csv @@ -0,0 +1,24 @@ +type,request,tool,response +fan,Schalte den ein,HassTurnOn,Ich habe den Ventilator eingeschaltet. +fan,Schalte die Ventilatoren in der ein.,HassTurnOn,Ich habe den Ventilator in der eingeschaltet. +fan,Schalte den aus,HassTurnOff,Ich habe den Ventilator ausgeschaltet. +fan,Schalte den Ventilator um,HassToggle,Ich habe den Ventilator umgeschaltet. +light,Schalte das Licht an,HassTurnOn,Ich habe das Licht eingeschaltet. +light,Schalte das Licht aus,HassTurnOff,Ich habe das Licht ausgeschaltet. +light,Schalte das Licht in aus,HassTurnOff,Ich habe das Licht in ausgeschaltet. +light,Schalte das Licht in um,HassToggle,Ich habe das Licht in umgeschaltet. +light,Schalte die Helligkeit von auf ,HassLightSet,Ich habe die Helligkeit umgeschaltet. +light,Schalte die Lichter in auf ,HassLightSet,Ich habe die Farbe jetzt geändert. +media_player,Schalte den ein,HassTurnOn,Ich schalte den Media-Player für dich ein. +media_player, sollte ausgeschaltet werden,HassTurnOff,Ich habe den Media-Player ausgeschaltet. +media_player,Drücke den Play-Knopf am ,HassMediaUnpause,Starte die Medienwiedergabe. +media_player,Pausiere den ,HassMediaPause,Pausiere die Medienwiedergabe. +media_player,Spiele das nächste auf ,HassMediaNext,Überspringe zum nächsten Track. +switch,Schalte den ein,HassTurnOn,Ich habe den Schalter eingeschaltet. +switch,Schalte den aus,HassTurnOff,Ich habe den Schalter ausgeschaltet. +switch,Schalte die Schalter in der aus,HassTurnOff,Ich habe die Geräte wie gewünscht ausgeschaltet. +switch,Schalte den Schalter um,HassToggle,Ich habe den Schalter umgeschaltet. +vacuum,Starte den Staubsauger namens ,HassVacuumStart,Ich habe den Staubsauger gestartet. +vacuum,Stoppe den Staubsauger ,HassVacuumReturnToBase,Ich habe den Staubsauger zurück zur Basis geschickt. +blinds, Schliesse den Rolladen , HassSetPosition, Ich habe den Rolladen geschlossen. +blinds, Öffne den Rolladen , HassSetPosition, Ich habe den Rolladen geöffnet. diff --git a/custom_components/llama_conversation/manifest.json b/custom_components/llama_conversation/manifest.json index a36be09..80ef3d1 100644 --- a/custom_components/llama_conversation/manifest.json +++ b/custom_components/llama_conversation/manifest.json @@ -1,7 +1,7 @@ { "domain": "llama_conversation", "name": "Local LLM Conversation", - "version": "0.3.6", + "version": "0.3.7", "codeowners": ["@acon96"], "config_flow": true, "dependencies": ["conversation"], @@ -10,7 +10,7 @@ "integration_type": "service", "iot_class": "local_polling", "requirements": [ - "huggingface-hub==0.23.0", - "webcolors<=1.13" + "huggingface-hub>=0.23.0", + "webcolors>=24.8.0" ] } diff --git a/custom_components/llama_conversation/utils.py b/custom_components/llama_conversation/utils.py index d8cbedd..1b9281d 100644 --- a/custom_components/llama_conversation/utils.py +++ b/custom_components/llama_conversation/utils.py @@ -1,11 +1,13 @@ import time import os +import re import sys import platform import logging import multiprocessing import voluptuous as vol import webcolors +from webcolors import CSS3 from importlib.metadata import version from homeassistant.helpers import config_validation as cv @@ -21,6 +23,12 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +CSS3_NAME_TO_RGB = { + name: webcolors.name_to_rgb(name, CSS3) + for name + in webcolors.names(CSS3) +} + class MissingQuantizationException(Exception): def __init__(self, missing_quant: str, available_quants: list[str]): self.missing_quant = missing_quant @@ -28,8 +36,9 @@ class MissingQuantizationException(Exception): def closest_color(requested_color): min_colors = {} - for key, name in webcolors.CSS3_HEX_TO_NAMES.items(): - r_c, g_c, b_c = webcolors.hex_to_rgb(key) + + for name, rgb in CSS3_NAME_TO_RGB.items(): + r_c, g_c, b_c = rgb rd = (r_c - requested_color[0]) ** 2 gd = (g_c - requested_color[1]) ** 2 bd = (b_c - requested_color[2]) ** 2 @@ -97,10 +106,13 @@ def download_model_from_hf(model_name: str, quantization_type: str, storage_fold fs = HfFileSystem() potential_files = [ f for f in fs.glob(f"{model_name}/*.gguf") ] - wanted_file = [f for f in potential_files if (f".{quantization_type.lower()}." in f or f".{quantization_type.upper()}." in f)] + wanted_file = [f for f in potential_files if (f"{quantization_type.lower()}.gguf" in f or f"{quantization_type.upper()}.gguf" in f)] if len(wanted_file) != 1: - available_quants = [file.split(".")[-2].upper() for file in potential_files] + available_quants = [ + re.split(r"\.|-", file.removesuffix(".gguf"))[-1].upper() + for file in potential_files + ] raise MissingQuantizationException(quantization_type, available_quants) try: os.makedirs(storage_folder, exist_ok=True) @@ -138,6 +150,11 @@ def validate_llama_cpp_python_installation(): if process.exitcode != 0: raise Exception(f"Failed to properly initialize llama-cpp-python. (Exit code {process.exitcode}.)") +def get_llama_cpp_python_version(): + if not is_installed("llama-cpp-python"): + return None + return version("llama-cpp-python") + def install_llama_cpp_python(config_dir: str): installed_wrong_version = False diff --git a/hacs.json b/hacs.json index cc89ed4..80c74e1 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "Local LLM Conversation", - "homeassistant": "2024.8.0", + "homeassistant": "2024.12.3", "content_in_root": false, "render_readme": true } diff --git a/requirements.txt b/requirements.txt index 20067cb..db89333 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,9 +14,8 @@ langcodes babel==2.15.0 # integration requirements -requests>=2.31.0 -huggingface-hub==0.23.0 -webcolors==1.13 +huggingface-hub>=0.23.0 +webcolors>=24.8.0 # types from Home Assistant homeassistant>=2024.6.1 diff --git a/scripts/run_docker_to_make_wheels.sh b/scripts/run_docker_to_make_wheels.sh index 4022218..caed50f 100755 --- a/scripts/run_docker_to_make_wheels.sh +++ b/scripts/run_docker_to_make_wheels.sh @@ -1,15 +1,21 @@ #!/bin/bash -VERSION_TO_BUILD="v0.2.88" +VERSION_TO_BUILD="v0.3.5" -# make python11 wheels +# make python 11 wheels # docker run -it --rm \ # --entrypoint bash \ # -v $(pwd):/tmp/dist \ # homeassistant/home-assistant:2023.12.4 /tmp/dist/make_wheel.sh $VERSION_TO_BUILD # make python 12 wheels +# docker run -it --rm \ +# --entrypoint bash \ +# -v $(pwd):/tmp/dist \ +# homeassistant/home-assistant:2024.2.1 /tmp/dist/make_wheel.sh $VERSION_TO_BUILD + +# make python 13 wheels docker run -it --rm \ --entrypoint bash \ -v $(pwd):/tmp/dist \ - homeassistant/home-assistant:2024.2.1 /tmp/dist/make_wheel.sh $VERSION_TO_BUILD \ No newline at end of file + homeassistant/home-assistant:2024.12.3 /tmp/dist/make_wheel.sh $VERSION_TO_BUILD \ No newline at end of file