Skip to content

Commit

Permalink
Tool selection
Browse files Browse the repository at this point in the history
  • Loading branch information
Shulyaka committed Dec 9, 2024
1 parent 121ea6c commit 0ad0060
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 15 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ This integration provides:
1. HTTP API for available LLM tools to integrate HA LLM Tools with an externally running LLM
2. Framework to easily add new LLM tools from other custom integrations, making Home Assistant a platform for LLM tools experimentation.
3. Enhanced and experimental versions of core 'Assist' LLM tools
4. Extra LLM tools:
4. Selectively Enable/Disable any tool
5. Extra LLM tools:
* Web, maps, and news search with Duck Duck Go
* Permanent memory tool
* Python code execution
Expand Down Expand Up @@ -42,7 +43,7 @@ There are following configuration options available:
Power LLM includes a tool that allows LLM to write scripts in Home Assistant format and instantly execute them to handle more complex tasks than covered by standard intents. If this option is enabled, Power LLM will make an effort to verify that all entities referenced in this script are exposed. This process however has certain limitations (for example if the entity id is evaluated from template at runtime), so the script might fail this check more often than wanted.

* ### Facts that the model remembers for each user
This field should be in a yaml map format, with user_id as the key and string as the value. The string contains the facts that the LLM chose to remember about the user. The best way to add things here is to ask LLM to remember something about you. This option is presented here in case you want to delete something.
These field contain the facts that the LLM chose to remember about the user for each `user_id`. You can also ask LLM to remember something about you. This option is presented here in case you want to delete something.

## HTTP API

Expand Down Expand Up @@ -72,7 +73,6 @@ There are two options:

## TODO

* Selectively Enable/Disable any tool
* Weather forecast intent
* Web scrapping using trafilatura
* Ability to talk to other conversation agents (i.e. "Ask expert" for a reasoning model, or NLP conversation (Assist) for device control fallback)
Expand Down
12 changes: 9 additions & 3 deletions custom_components/powerllm/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN
from homeassistant.components.weather.intent import INTENT_GET_WEATHER
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.const import CONF_DEFAULT, CONF_NAME
from homeassistant.core import HomeAssistant, callback, split_entity_id
from homeassistant.helpers import (
area_registry as ar,
Expand All @@ -29,6 +29,7 @@
CONF_INTENT_ENTITIES,
CONF_PROMPT_ENTITIES,
CONF_SCRIPT_EXPOSED_ONLY,
CONF_TOOL_SELECTION,
DOMAIN,
)
from .llm_tools import PowerIntentTool, PowerLLMTool, PowerScriptTool
Expand Down Expand Up @@ -193,7 +194,7 @@ def _async_get_tools(
}

if not self.config_entry.options[CONF_INTENT_ENTITIES]:
ignore_intents.append(intent.INTENT_GET_STATE)
ignore_intents = ignore_intents | {intent.INTENT_GET_STATE}

intent_handlers = [
intent_handler
Expand Down Expand Up @@ -245,6 +246,11 @@ def _async_get_tools(

tools.extend(self.hass.data.get(DOMAIN, {}).values())

tool_selection = self.config_entry.options.get(CONF_TOOL_SELECTION, {})
tool_selection_default = tool_selection.get(CONF_DEFAULT, True)
return [
tool for tool in tools if tool.async_is_applicable(self.hass, llm_context)
tool
for tool in tools
if tool.async_is_applicable(self.hass, llm_context)
and tool_selection.get(tool.name, tool_selection_default)
]
41 changes: 38 additions & 3 deletions custom_components/powerllm/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv, selector
from homeassistant.const import CONF_DEFAULT, CONF_NAME
from homeassistant.core import Context, callback
from homeassistant.helpers import config_validation as cv, llm, selector

from .api import PowerLLMAPI
from .const import (
CONF_DUCKDUCKGO_REGION,
CONF_INTENT_ENTITIES,
CONF_MEMORY_PROMPTS,
CONF_PROMPT_ENTITIES,
CONF_SCRIPT_EXPOSED_ONLY,
CONF_TOOL_SELECTION,
DOMAIN,
)

Expand Down Expand Up @@ -130,6 +132,36 @@ async def get_data_schema(self) -> vol.Schema:

async def get_options_schema(self) -> vol.Schema:
"""Get data schema."""
tmp_entry = ConfigEntry(
discovery_keys={},
domain=DOMAIN,
minor_version=0,
source="",
title="Temp",
unique_id=None,
version=0,
data={CONF_NAME: "Temp"},
options={
CONF_PROMPT_ENTITIES: False,
CONF_INTENT_ENTITIES: True,
CONF_DUCKDUCKGO_REGION: "wt-wt",
CONF_SCRIPT_EXPOSED_ONLY: False,
},
)
tmp_context = llm.LLMContext(
platform=DOMAIN,
context=Context(user_id="Temp"),
user_prompt=None,
language=None,
assistant=None,
device_id=None,
)
tmp_api = await PowerLLMAPI(self.hass, tmp_entry).async_get_api_instance(
tmp_context
)
tools = [tool.name for tool in tmp_api.tools]
tools.append(CONF_DEFAULT)

return vol.Schema(
{
vol.Required(CONF_PROMPT_ENTITIES, default=True): bool,
Expand Down Expand Up @@ -160,6 +192,9 @@ async def get_options_schema(self) -> vol.Schema:
if not user.system_generated
}
),
vol.Optional(CONF_TOOL_SELECTION): vol.Schema(
{vol.Required(tool, default=True): bool for tool in tools}
),
}
)

Expand Down
1 change: 1 addition & 0 deletions custom_components/powerllm/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
CONF_DUCKDUCKGO_REGION = "duckduckgo_region"
CONF_SCRIPT_EXPOSED_ONLY = "script_exposed_only"
CONF_MEMORY_PROMPTS = "memory_prompts"
CONF_TOOL_SELECTION = "tool_selection"
20 changes: 20 additions & 0 deletions custom_components/powerllm/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@
"memory_prompts": {
"title": "Memory tool prompts",
"description": "Enter facts that the system should know about each user."
},
"tool_selection": {
"title": "Tool selection",
"description": "Select tools available for this API.",
"data": {
"default": "Default"
},
"data_description": {
"default": "Other tools not listed above"
}
}
}
},
Expand All @@ -43,6 +53,16 @@
"memory_prompts": {
"title": "Memory tool prompts",
"description": "Enter facts that the system should know about each user."
},
"tool_selection": {
"title": "Tool selection",
"description": "Select tools available for this API.",
"data": {
"default": "Default"
},
"data_description": {
"default": "Other tools not listed above"
}
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion custom_components/powerllm/tools/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ def __init__(self, config_entry):
@callback
def async_is_applicable(self, hass: HomeAssistant, llm_context: LLMContext) -> bool:
"""Check the tool applicability."""
return llm_context.context.user_id is not None
return (
llm_context.context is not None and llm_context.context.user_id is not None
)

@callback
def prompt(self, hass: HomeAssistant, llm_context: LLMContext) -> str | None:
Expand Down
20 changes: 20 additions & 0 deletions custom_components/powerllm/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@
"memory_prompts": {
"title": "Memory tool prompts",
"description": "Enter facts that the system should know about each user."
},
"tool_selection": {
"title": "Tool selection",
"description": "Select tools available for this API.",
"data": {
"default": "Default"
},
"data_description": {
"default": "Other tools not listed above"
}
}
}
},
Expand All @@ -43,6 +53,16 @@
"memory_prompts": {
"title": "Memory tool prompts",
"description": "Enter facts that the system should know about each user."
},
"tool_selection": {
"title": "Tool selection",
"description": "Select tools available for this API.",
"data": {
"default": "Default"
},
"data_description": {
"default": "Other tools not listed above"
}
}
}
}
Expand Down
16 changes: 15 additions & 1 deletion tests/const.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""Constants for powerllm tests."""

from homeassistant.const import CONF_NAME
from homeassistant.const import CONF_DEFAULT, CONF_NAME

from custom_components.powerllm.const import (
CONF_DUCKDUCKGO_REGION,
CONF_INTENT_ENTITIES,
CONF_MEMORY_PROMPTS,
CONF_PROMPT_ENTITIES,
CONF_SCRIPT_EXPOSED_ONLY,
CONF_TOOL_SELECTION,
)

# Mock config data to be used across multiple tests
Expand All @@ -21,4 +22,17 @@
CONF_DUCKDUCKGO_REGION: "wt-wt",
CONF_SCRIPT_EXPOSED_ONLY: True,
CONF_MEMORY_PROMPTS: {},
CONF_TOOL_SELECTION: {
"HassCancelAllTimers": True,
"HassGetState": True,
"HassSetPosition": True,
"HassTurnOff": True,
"HassTurnOn": True,
"homeassistant_script": True,
"maps_search": True,
"memory": True,
"news": True,
"websearch": True,
CONF_DEFAULT: True,
},
}
28 changes: 24 additions & 4 deletions tests/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.const import CONF_DEFAULT
from homeassistant.core import HomeAssistant

from custom_components.powerllm.const import (
CONF_DUCKDUCKGO_REGION,
CONF_INTENT_ENTITIES,
CONF_PROMPT_ENTITIES,
CONF_SCRIPT_EXPOSED_ONLY,
CONF_TOOL_SELECTION,
DOMAIN,
)

Expand Down Expand Up @@ -50,15 +52,26 @@ async def test_config_flow(hass: HomeAssistant):
assert result["step_id"] == "init"

# Advance to step 3
user_input = MOCK_OPTIONS_CONFIG.copy()
user_input.pop("memory_prompts", None)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=user_input
result["flow_id"],
user_input={
k: v
for k, v in MOCK_OPTIONS_CONFIG.items()
if k not in {"memory_prompts", "tool_selection"}
},
)

assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "memory_prompts"

# Advance to step 4
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_OPTIONS_CONFIG.get(result["step_id"], {})
)

assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "tool_selection"

# Final result
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_OPTIONS_CONFIG.get(result["step_id"], {})
Expand Down Expand Up @@ -97,9 +110,16 @@ async def test_options_flow(
options["flow_id"],
{},
)
# await hass.async_block_till_done()
assert options["type"] == data_entry_flow.RESULT_TYPE_FORM
assert options["step_id"] == "tool_selection"

options = await hass.config_entries.options.async_configure(
options["flow_id"],
{"default": True},
)
assert options["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert options["data"][CONF_PROMPT_ENTITIES] is False
assert options["data"][CONF_INTENT_ENTITIES] is False
assert options["data"][CONF_DUCKDUCKGO_REGION] == "us-en"
assert options["data"][CONF_SCRIPT_EXPOSED_ONLY] is False
assert options["data"][CONF_TOOL_SELECTION][CONF_DEFAULT] is True

0 comments on commit 0ad0060

Please sign in to comment.