Improve LLM tool descriptions for brightness and volume percentage (#138685)

* Improve tool descriptions for brightness and volume percentage

* Address lint errors

* Update intent.py to revert of a light

* Create explicit types to make intent slots more future proof

* Remove comments about slot type

---------

Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
Allen Porter 2025-03-08 19:28:35 -08:00 committed by GitHub
parent f0c5e00cc1
commit 6675b497bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 74 additions and 41 deletions

View File

@ -28,13 +28,21 @@ async def async_setup_intents(hass: HomeAssistant) -> None:
DOMAIN,
SERVICE_TURN_ON,
optional_slots={
("color", ATTR_RGB_COLOR): color_util.color_name_to_rgb,
("temperature", ATTR_COLOR_TEMP_KELVIN): cv.positive_int,
("brightness", ATTR_BRIGHTNESS_PCT): vol.All(
vol.Coerce(int), vol.Range(0, 100)
"color": intent.IntentSlotInfo(
service_data_name=ATTR_RGB_COLOR,
value_schema=color_util.color_name_to_rgb,
),
"temperature": intent.IntentSlotInfo(
service_data_name=ATTR_COLOR_TEMP_KELVIN,
value_schema=cv.positive_int,
),
"brightness": intent.IntentSlotInfo(
service_data_name=ATTR_BRIGHTNESS_PCT,
description="The brightness percentage of the light between 0 and 100, where 0 is off and 100 is fully lit",
value_schema=vol.All(vol.Coerce(int), vol.Range(0, 100)),
),
},
description="Sets the brightness or color of a light",
description="Sets the brightness percentage or color of a light",
platforms={DOMAIN},
),
)

View File

@ -96,11 +96,16 @@ async def async_setup_intents(hass: HomeAssistant) -> None:
required_states={MediaPlayerState.PLAYING},
required_features=MediaPlayerEntityFeature.VOLUME_SET,
required_slots={
ATTR_MEDIA_VOLUME_LEVEL: vol.All(
vol.Coerce(int), vol.Range(min=0, max=100), lambda val: val / 100
)
ATTR_MEDIA_VOLUME_LEVEL: intent.IntentSlotInfo(
description="The volume percentage of the media player",
value_schema=vol.All(
vol.Coerce(int),
vol.Range(min=0, max=100),
lambda val: val / 100,
),
),
},
description="Sets the volume of a media player",
description="Sets the volume percentage of a media player",
platforms={DOMAIN},
device_classes={MediaPlayerDeviceClass},
),

View File

@ -38,7 +38,7 @@ from .typing import VolSchemaType
_LOGGER = logging.getLogger(__name__)
type _SlotsType = dict[str, Any]
type _IntentSlotsType = dict[
str | tuple[str, str], VolSchemaType | Callable[[Any], Any]
str | tuple[str, str], IntentSlotInfo | VolSchemaType | Callable[[Any], Any]
]
INTENT_TURN_OFF = "HassTurnOff"
@ -874,6 +874,34 @@ def non_empty_string(value: Any) -> str:
return value_str
@dataclass(kw_only=True)
class IntentSlotInfo:
"""Details about how intent slots are processed and validated."""
service_data_name: str | None = None
"""Optional name of the service data input to map to this slot."""
description: str | None = None
"""Human readable description of the slot."""
value_schema: VolSchemaType | Callable[[Any], Any] = vol.Any
"""Validator for the slot."""
def _convert_slot_info(
key: str | tuple[str, str],
value: IntentSlotInfo | VolSchemaType | Callable[[Any], Any],
) -> tuple[str, IntentSlotInfo]:
"""Create an IntentSlotInfo from the various supported input arguments."""
if isinstance(value, IntentSlotInfo):
if not isinstance(key, str):
raise TypeError("Tuple key and IntentSlotDescription value not supported")
return key, value
if isinstance(key, tuple):
return key[0], IntentSlotInfo(service_data_name=key[1], value_schema=value)
return key, IntentSlotInfo(value_schema=value)
class DynamicServiceIntentHandler(IntentHandler):
"""Service Intent handler registration (dynamic).
@ -907,23 +935,14 @@ class DynamicServiceIntentHandler(IntentHandler):
self.platforms = platforms
self.device_classes = device_classes
self.required_slots: _IntentSlotsType = {}
if required_slots:
for key, value_schema in required_slots.items():
if isinstance(key, str):
# Slot name/service data key
key = (key, key)
self.required_slots[key] = value_schema
self.optional_slots: _IntentSlotsType = {}
if optional_slots:
for key, value_schema in optional_slots.items():
if isinstance(key, str):
# Slot name/service data key
key = (key, key)
self.optional_slots[key] = value_schema
self.required_slots: dict[str, IntentSlotInfo] = dict(
_convert_slot_info(key, value)
for key, value in (required_slots or {}).items()
)
self.optional_slots: dict[str, IntentSlotInfo] = dict(
_convert_slot_info(key, value)
for key, value in (optional_slots or {}).items()
)
@cached_property
def slot_schema(self) -> dict:
@ -964,16 +983,20 @@ class DynamicServiceIntentHandler(IntentHandler):
if self.required_slots:
slot_schema.update(
{
vol.Required(key[0]): validator
for key, validator in self.required_slots.items()
vol.Required(
key, description=slot_info.description
): slot_info.value_schema
for key, slot_info in self.required_slots.items()
}
)
if self.optional_slots:
slot_schema.update(
{
vol.Optional(key[0]): validator
for key, validator in self.optional_slots.items()
vol.Optional(
key, description=slot_info.description
): slot_info.value_schema
for key, slot_info in self.optional_slots.items()
}
)
@ -1156,18 +1179,15 @@ class DynamicServiceIntentHandler(IntentHandler):
service_data: dict[str, Any] = {ATTR_ENTITY_ID: state.entity_id}
if self.required_slots:
service_data.update(
{
key[1]: intent_obj.slots[key[0]]["value"]
for key in self.required_slots
}
)
for key, slot_info in self.required_slots.items():
service_data[slot_info.service_data_name or key] = intent_obj.slots[
key
]["value"]
if self.optional_slots:
for key in self.optional_slots:
value = intent_obj.slots.get(key[0])
if value:
service_data[key[1]] = value["value"]
for key, slot_info in self.optional_slots.items():
if value := intent_obj.slots.get(key):
service_data[slot_info.service_data_name or key] = value["value"]
await self._run_then_background(
hass.async_create_task_internal(