From 6675b497bd8680b8b1d54fd9b117af8ce288f0b8 Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
Date: Sat, 8 Mar 2025 19:28:35 -0800
Subject: [PATCH] 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>
---
 homeassistant/components/light/intent.py      | 18 ++--
 .../components/media_player/intent.py         | 13 ++-
 homeassistant/helpers/intent.py               | 84 ++++++++++++-------
 3 files changed, 74 insertions(+), 41 deletions(-)

diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py
index 83f2ee58b5e..250e1f5b2c1 100644
--- a/homeassistant/components/light/intent.py
+++ b/homeassistant/components/light/intent.py
@@ -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},
         ),
     )
diff --git a/homeassistant/components/media_player/intent.py b/homeassistant/components/media_player/intent.py
index edfab2a668f..af37c0d68bb 100644
--- a/homeassistant/components/media_player/intent.py
+++ b/homeassistant/components/media_player/intent.py
@@ -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},
         ),
diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py
index 0bb96615d3f..75572194bb8 100644
--- a/homeassistant/helpers/intent.py
+++ b/homeassistant/helpers/intent.py
@@ -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(
-- 
GitLab