Skip to content

Commit

Permalink
Merge pull request #199 from acon96/release/v0.3.4-fixed
Browse files Browse the repository at this point in the history
Release v0.3.4 (fixed)
  • Loading branch information
acon96 authored Aug 12, 2024
2 parents 80e07fd + 47ce389 commit f6cb969
Show file tree
Hide file tree
Showing 28 changed files with 3,014 additions and 138 deletions.
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ 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**

In order to integrate with Home Assistant, we provide a custom component that exposes the locally running LLM as a "conversation agent".

This component can be interacted with in a few ways:
Expand Down Expand Up @@ -90,6 +92,25 @@ The dataset is available on HuggingFace: https://huggingface.co/datasets/acon96/
The source for the dataset is in the [data](/data) of this repository.

### Training

If you want to prepare your own testing environment, see the details on how to do it.

<details>
<summary>Prepare environment</summary>

Start by installing system dependencies:
`sudo apt-get install python3-dev`

Then create a Python virtual environment and install all necessary library:
```
python3 -m venv .train_data
source ./.train_data/bin/activate
pip3 install datasets==2.20.0 dataclasses==0.6 transformers==4.43.3 torch==2.4.0 accelerate==0.33.0 tensorboard==2.17.0
```

</details>


The 3B model was trained as a full fine-tuning on 2x RTX 4090 (48GB). Training time took approximately 28 hours. It was trained on the `--large` dataset variant.

<details>
Expand Down Expand Up @@ -136,7 +157,7 @@ In order to facilitate running the project entirely on the system where Home Ass
## Version History
| Version | Description |
|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| v0.3.4 | Update bundled llama-cpp-python to support new models, various bug fixes |
| v0.3.4 | Significantly improved language support including full Polish translation, Update bundled llama-cpp-python to support new models, various bug fixes |
| v0.3.3 | Improvements to the Generic OpenAI Backend, improved area handling, fix issue using RGB colors, remove EOS token from responses, replace requests dependency with aiohttp included with Home Assistant |
| v0.3.2 | Fix for exposed script entities causing errors, fix missing GBNF error, trim whitespace from model output |
| v0.3.1 | Adds basic area support in prompting, Fix for broken requirements, fix for issue with formatted tools, fix custom API not registering on startup properly |
Expand Down
65 changes: 7 additions & 58 deletions custom_components/llama_conversation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,16 @@

import homeassistant.components.conversation as ha_conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, llm
from homeassistant.util.json import JsonObjectType

import voluptuous as vol

from .agent import (
LocalLLMAgent,
LlamaCppAgent,
GenericOpenAIAPIAgent,
TextGenerationWebuiAgent,
LlamaCppPythonAPIAgent,
OllamaAPIAgent,
)

from .const import (
CONF_BACKEND_TYPE,
DEFAULT_BACKEND_TYPE,
BACKEND_TYPE_LLAMA_HF,
BACKEND_TYPE_LLAMA_EXISTING,
BACKEND_TYPE_TEXT_GEN_WEBUI,
BACKEND_TYPE_GENERIC_OPENAI,
BACKEND_TYPE_LLAMA_CPP_PYTHON_SERVER,
BACKEND_TYPE_OLLAMA,
ALLOWED_SERVICE_CALL_ARGUMENTS,
DOMAIN,
HOME_LLM_API_ID,
Expand All @@ -42,61 +26,26 @@

CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)

async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Handle options update."""
hass.data[DOMAIN][entry.entry_id] = entry

# call update handler
agent: LocalLLMAgent = ha_conversation.get_agent_manager(hass).async_get_agent(entry.entry_id)
await hass.async_add_executor_job(agent._update_options)
PLATFORMS = (Platform.CONVERSATION,)

return True

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Local LLM Conversation from a config entry."""

# make sure the API is registered
if not any([x.id == HOME_LLM_API_ID for x in llm.async_get_apis(hass)]):
llm.async_register_api(hass, HomeLLMAPI(hass))

def create_agent(backend_type):
agent_cls = None

if backend_type in [ BACKEND_TYPE_LLAMA_HF, BACKEND_TYPE_LLAMA_EXISTING ]:
agent_cls = LlamaCppAgent
elif backend_type == BACKEND_TYPE_GENERIC_OPENAI:
agent_cls = GenericOpenAIAPIAgent
elif backend_type == BACKEND_TYPE_TEXT_GEN_WEBUI:
agent_cls = TextGenerationWebuiAgent
elif backend_type == BACKEND_TYPE_LLAMA_CPP_PYTHON_SERVER:
agent_cls = LlamaCppPythonAPIAgent
elif backend_type == BACKEND_TYPE_OLLAMA:
agent_cls = OllamaAPIAgent

return agent_cls(hass, entry)

# create the agent in an executor job because the constructor calls `open()`
backend_type = entry.data.get(CONF_BACKEND_TYPE, DEFAULT_BACKEND_TYPE)
agent = await hass.async_add_executor_job(create_agent, backend_type)

# call load model
await agent._async_load_model(entry)

# handle updates to the options
entry.async_on_unload(entry.add_update_listener(update_listener))

ha_conversation.async_set_agent(hass, entry, agent)

hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = entry
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Local LLM."""
"""Unload Ollama."""
if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
return False
hass.data[DOMAIN].pop(entry.entry_id)
ha_conversation.async_unset_agent(hass, entry)
return True

async def async_migrate_entry(hass, config_entry: ConfigEntry):
Expand Down
19 changes: 19 additions & 0 deletions custom_components/llama_conversation/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@
DEFAULT_SSL,
DEFAULT_MAX_TOKENS,
PERSONA_PROMPTS,
CURRENT_DATE_PROMPT,
DEVICES_PROMPT,
SERVICES_PROMPT,
TOOLS_PROMPT,
AREA_PROMPT,
USER_INSTRUCTION,
DEFAULT_PROMPT_BASE,
DEFAULT_TEMPERATURE,
DEFAULT_TOP_K,
Expand Down Expand Up @@ -681,7 +687,20 @@ async def async_step_model_parameters(
selected_default_options.update(OPTIONS_OVERRIDES[key])

persona = PERSONA_PROMPTS.get(self.selected_language, PERSONA_PROMPTS.get("en"))
current_date = CURRENT_DATE_PROMPT.get(self.selected_language, CURRENT_DATE_PROMPT.get("en"))
devices = DEVICES_PROMPT.get(self.selected_language, DEVICES_PROMPT.get("en"))
services = SERVICES_PROMPT.get(self.selected_language, SERVICES_PROMPT.get("en"))
tools = TOOLS_PROMPT.get(self.selected_language, TOOLS_PROMPT.get("en"))
area = AREA_PROMPT.get(self.selected_language, AREA_PROMPT.get("en"))
user_instruction = USER_INSTRUCTION.get(self.selected_language, USER_INSTRUCTION.get("en"))

selected_default_options[CONF_PROMPT] = selected_default_options[CONF_PROMPT].replace("<persona>", persona)
selected_default_options[CONF_PROMPT] = selected_default_options[CONF_PROMPT].replace("<current_date>", current_date)
selected_default_options[CONF_PROMPT] = selected_default_options[CONF_PROMPT].replace("<devices>", devices)
selected_default_options[CONF_PROMPT] = selected_default_options[CONF_PROMPT].replace("<services>", services)
selected_default_options[CONF_PROMPT] = selected_default_options[CONF_PROMPT].replace("<tools>", tools)
selected_default_options[CONF_PROMPT] = selected_default_options[CONF_PROMPT].replace("<area>", area)
selected_default_options[CONF_PROMPT] = selected_default_options[CONF_PROMPT].replace("<user_instruction>", user_instruction)

schema = vol.Schema(local_llama_config_option_schema(self.hass, selected_default_options, backend_type))

Expand Down
63 changes: 53 additions & 10 deletions custom_components/llama_conversation/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,67 @@
"de": "Du bist \u201eAl\u201c, ein hilfreicher KI-Assistent, der die Ger\u00e4te in einem Haus steuert. F\u00fchren Sie die folgende Aufgabe gem\u00e4\u00df den Anweisungen durch oder beantworten Sie die folgende Frage nur mit den bereitgestellten Informationen.",
"fr": "Vous \u00eates \u00ab\u00a0Al\u00a0\u00bb, un assistant IA utile qui contr\u00f4le les appareils d'une maison. Effectuez la t\u00e2che suivante comme indiqu\u00e9 ou r\u00e9pondez \u00e0 la question suivante avec les informations fournies uniquement.",
"es": "Eres 'Al', un \u00fatil asistente de IA que controla los dispositivos de una casa. Complete la siguiente tarea seg\u00fan las instrucciones o responda la siguiente pregunta \u00fanicamente con la informaci\u00f3n proporcionada.",
"pl": "Jeste\u015b 'Al', pomocnym asystentem AI, kt\u00f3ry kontroluje urz\u0105dzenia w domu. Wykonaj poni\u017csze zadanie zgodnie z instrukcj\u0105 lub odpowiedz na poni\u017csze pytanie, korzystaj\u0105c wy\u0142\u0105cznie z podanych informacji."
}
CURRENT_DATE_PROMPT = {
"en": """The current time and date is {{ (as_timestamp(now()) | timestamp_custom("%I:%M %p on %A %B %d, %Y", "")) }}""",
"de": """{% set day_name = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"] %}{% set month_name = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"] %}Die aktuelle Uhrzeit und das aktuelle Datum sind {{ (as_timestamp(now()) | timestamp_custom("%H:%M", local=True)) }} {{ day_name[now().weekday()] }}, {{ now().day }} {{ month_name[now().month -1]}} {{ now().year }}.""",
"fr": """{% set day_name = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"] %}{% set month_name = ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"] %} L'heure et la date actuelles sont {{ (as_timestamp(now()) | timestamp_custom("%H:%M", local=True)) }} {{ day_name[now().weekday()] }}, {{ now().day }} {{ month_name[now().month -1]}} {{ now().year }}.""",
"es": """{% set day_name = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"] %}{% set month_name = ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"] %}La hora y fecha actuales son {{ (as_timestamp(now()) | timestamp_custom("%H:%M", local=True)) }} {{ day_name[now().weekday()] }}, {{ now().day }} de {{ month_name[now().month -1]}} de {{ now().year }}.""",
"pl": """{% set day_name = ["poniedziałek", "wtorek", "środę", "czwartek", "piątek", "sobotę", "niedzielę"] %}{% set month_name = ["styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec", "lipiec", "sierpień", "wrzesień", "październik", "listopad", "grudzień"] %}Aktualna godzina i data to {{ (as_timestamp(now()) | timestamp_custom("%H:%M", local=True)) }} w {{ day_name[now().weekday()] }}, {{ now().day }} {{ month_name[now().month -1]}} {{ now().year }}."""
}
DEVICES_PROMPT = {
"en": "Devices",
"de": "Ger\u00e4te",
"fr": "Appareils",
"es": "Dispositivos",
"pl": "Urządzenia",
}
SERVICES_PROMPT = {
"en": "Services",
"de": "Dienste",
"fr": "Services",
"es": "Servicios",
"pl": "Usługi",
}
TOOLS_PROMPT = {
"en": "Tools",
"de": "Werkzeuge",
"fr": "Outils",
"es": "Herramientas",
"pl": "Narzędzia",
}
AREA_PROMPT = {
"en": "Area",
"de": "Bereich",
"fr": "Zone",
"es": "Área",
"pl": "Obszar",
}
USER_INSTRUCTION = {
"en": "User instruction",
"de": "Benutzeranweisung",
"fr": "Instruction de l'utilisateur ",
"es": "Instrucción del usuario",
"pl": "Instrukcja użytkownika"
}
DEFAULT_PROMPT_BASE = """<persona>
The current time and date is {{ (as_timestamp(now()) | timestamp_custom("%I:%M %p on %A %B %d, %Y", "")) }}
Tools: {{ tools | to_json }}
Devices:
<current_date>
<tools>: {{ tools | to_json }}
<devices>:
{% for device in devices | selectattr('area_id', 'none'): %}
{{ device.entity_id }} '{{ device.name }}' = {{ device.state }}{{ ([""] + device.attributes) | join(";") }}
{% endfor %}
{% for area in devices | rejectattr('area_id', 'none') | groupby('area_name') %}
## Area: {{ area.grouper }}
## <area>: {{ area.grouper }}
{% for device in area.list %}
{{ device.entity_id }} '{{ device.name }}' = {{ device.state }};{{ device.attributes | join(";") }}
{% endfor %}
{% endfor %}"""
DEFAULT_PROMPT_BASE_LEGACY = """<persona>
The current time and date is {{ (as_timestamp(now()) | timestamp_custom("%I:%M %p on %A %B %d, %Y", "")) }}
Services: {{ formatted_tools }}
Devices:
<current_date>
<services>: {{ formatted_tools }}
<devices>:
{{ formatted_devices }}"""
ICL_EXTRAS = """
{% for item in response_examples %}
Expand All @@ -41,7 +84,7 @@
{{ item.response }}
<functioncall> {{ item.tool | to_json }}
{% endfor %}
User instruction:"""
<user_instruction>:"""
DEFAULT_PROMPT = DEFAULT_PROMPT_BASE + ICL_EXTRAS
CONF_CHAT_MODEL = "huggingface_model"
DEFAULT_CHAT_MODEL = "acon96/Home-3B-v3-GGUF"
Expand Down Expand Up @@ -69,7 +112,7 @@
BACKEND_TYPE_OLLAMA = "ollama"
DEFAULT_BACKEND_TYPE = BACKEND_TYPE_LLAMA_HF
CONF_SELECTED_LANGUAGE = "selected_language"
CONF_SELECTED_LANGUAGE_OPTIONS = [ "en", "de", "fr", "es" ]
CONF_SELECTED_LANGUAGE_OPTIONS = [ "en", "de", "fr", "es", "pl"]
CONF_DOWNLOADED_MODEL_QUANTIZATION = "downloaded_model_quantization"
CONF_DOWNLOADED_MODEL_QUANTIZATION_OPTIONS = [
"Q4_0", "Q4_1", "Q5_0", "Q5_1", "IQ2_XXS", "IQ2_XS", "IQ2_S", "IQ2_M", "IQ1_S", "IQ1_M",
Expand Down Expand Up @@ -341,4 +384,4 @@
}

INTEGRATION_VERSION = "0.3.4"
EMBEDDED_LLAMA_CPP_PYTHON_VERSION = "0.2.84"
EMBEDDED_LLAMA_CPP_PYTHON_VERSION = "0.2.87"
Loading

0 comments on commit f6cb969

Please sign in to comment.