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, DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
optional_slots={ optional_slots={
("color", ATTR_RGB_COLOR): color_util.color_name_to_rgb, "color": intent.IntentSlotInfo(
("temperature", ATTR_COLOR_TEMP_KELVIN): cv.positive_int, service_data_name=ATTR_RGB_COLOR,
("brightness", ATTR_BRIGHTNESS_PCT): vol.All( value_schema=color_util.color_name_to_rgb,
vol.Coerce(int), vol.Range(0, 100) ),
"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}, platforms={DOMAIN},
), ),
) )

View File

@ -96,11 +96,16 @@ async def async_setup_intents(hass: HomeAssistant) -> None:
required_states={MediaPlayerState.PLAYING}, required_states={MediaPlayerState.PLAYING},
required_features=MediaPlayerEntityFeature.VOLUME_SET, required_features=MediaPlayerEntityFeature.VOLUME_SET,
required_slots={ required_slots={
ATTR_MEDIA_VOLUME_LEVEL: vol.All( ATTR_MEDIA_VOLUME_LEVEL: intent.IntentSlotInfo(
vol.Coerce(int), vol.Range(min=0, max=100), lambda val: val / 100 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}, platforms={DOMAIN},
device_classes={MediaPlayerDeviceClass}, device_classes={MediaPlayerDeviceClass},
), ),

View File

@ -38,7 +38,7 @@ from .typing import VolSchemaType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
type _SlotsType = dict[str, Any] type _SlotsType = dict[str, Any]
type _IntentSlotsType = dict[ type _IntentSlotsType = dict[
str | tuple[str, str], VolSchemaType | Callable[[Any], Any] str | tuple[str, str], IntentSlotInfo | VolSchemaType | Callable[[Any], Any]
] ]
INTENT_TURN_OFF = "HassTurnOff" INTENT_TURN_OFF = "HassTurnOff"
@ -874,6 +874,34 @@ def non_empty_string(value: Any) -> str:
return value_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): class DynamicServiceIntentHandler(IntentHandler):
"""Service Intent handler registration (dynamic). """Service Intent handler registration (dynamic).
@ -907,23 +935,14 @@ class DynamicServiceIntentHandler(IntentHandler):
self.platforms = platforms self.platforms = platforms
self.device_classes = device_classes self.device_classes = device_classes
self.required_slots: _IntentSlotsType = {} self.required_slots: dict[str, IntentSlotInfo] = dict(
if required_slots: _convert_slot_info(key, value)
for key, value_schema in required_slots.items(): for key, value in (required_slots or {}).items()
if isinstance(key, str): )
# Slot name/service data key self.optional_slots: dict[str, IntentSlotInfo] = dict(
key = (key, key) _convert_slot_info(key, value)
for key, value in (optional_slots or {}).items()
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
@cached_property @cached_property
def slot_schema(self) -> dict: def slot_schema(self) -> dict:
@ -964,16 +983,20 @@ class DynamicServiceIntentHandler(IntentHandler):
if self.required_slots: if self.required_slots:
slot_schema.update( slot_schema.update(
{ {
vol.Required(key[0]): validator vol.Required(
for key, validator in self.required_slots.items() key, description=slot_info.description
): slot_info.value_schema
for key, slot_info in self.required_slots.items()
} }
) )
if self.optional_slots: if self.optional_slots:
slot_schema.update( slot_schema.update(
{ {
vol.Optional(key[0]): validator vol.Optional(
for key, validator in self.optional_slots.items() 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} service_data: dict[str, Any] = {ATTR_ENTITY_ID: state.entity_id}
if self.required_slots: if self.required_slots:
service_data.update( for key, slot_info in self.required_slots.items():
{ service_data[slot_info.service_data_name or key] = intent_obj.slots[
key[1]: intent_obj.slots[key[0]]["value"] key
for key in self.required_slots ]["value"]
}
)
if self.optional_slots: if self.optional_slots:
for key in self.optional_slots: for key, slot_info in self.optional_slots.items():
value = intent_obj.slots.get(key[0]) if value := intent_obj.slots.get(key):
if value: service_data[slot_info.service_data_name or key] = value["value"]
service_data[key[1]] = value["value"]
await self._run_then_background( await self._run_then_background(
hass.async_create_task_internal( hass.async_create_task_internal(