Alexa Intent: Use the 'id' field and expose nearest resolutions as variables (#86709)

* Use the 'id' field and nearest resolutions
Expose nearest Resolution (ID and Value) as Variables

* Add more specific type hints

* Change type definition of request

* Add deprecation warning and remove variables

* Remove deprecation warning & update tests

* Fix wrong value assignment

* revert future changes
This commit is contained in:
Flo 2023-05-10 21:25:08 +02:00 committed by GitHub
parent 97cac66195
commit 0f2caf864a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 134 additions and 9 deletions

View File

@ -1,6 +1,7 @@
"""Support for Alexa skill service end point.""" """Support for Alexa skill service end point."""
import enum import enum
import logging import logging
from typing import Any
from homeassistant.components import http from homeassistant.components import http
from homeassistant.core import callback from homeassistant.core import callback
@ -180,12 +181,15 @@ async def async_handle_intent(hass, message):
return alexa_response.as_dict() return alexa_response.as_dict()
def resolve_slot_synonyms(key, request): def resolve_slot_data(key: str, request: dict[str, Any]) -> dict[str, str]:
"""Check slot request for synonym resolutions.""" """Check slot request for synonym resolutions."""
# Default to the spoken slot value if more than one or none are found. For # Default to the spoken slot value if more than one or none are found. Always
# passes the id and name of the nearest possible slot resolution. For
# reference to the request object structure, see the Alexa docs: # reference to the request object structure, see the Alexa docs:
# https://tinyurl.com/ybvm7jhs # https://tinyurl.com/ybvm7jhs
resolved_value = request["value"] resolved_data = {}
resolved_data["value"] = request["value"]
resolved_data["id"] = ""
if ( if (
"resolutions" in request "resolutions" in request
@ -200,20 +204,26 @@ def resolve_slot_synonyms(key, request):
if entry["status"]["code"] != SYN_RESOLUTION_MATCH: if entry["status"]["code"] != SYN_RESOLUTION_MATCH:
continue continue
possible_values.extend([item["value"]["name"] for item in entry["values"]]) possible_values.extend([item["value"] for item in entry["values"]])
# Always set id if available, otherwise an empty string is used as id
if len(possible_values) >= 1:
# Set ID if available
if "id" in possible_values[0]:
resolved_data["id"] = possible_values[0]["id"]
# If there is only one match use the resolved value, otherwise the # If there is only one match use the resolved value, otherwise the
# resolution cannot be determined, so use the spoken slot value # resolution cannot be determined, so use the spoken slot value and empty string as id
if len(possible_values) == 1: if len(possible_values) == 1:
resolved_value = possible_values[0] resolved_data["value"] = possible_values[0]["name"]
else: else:
_LOGGER.debug( _LOGGER.debug(
"Found multiple synonym resolutions for slot value: {%s: %s}", "Found multiple synonym resolutions for slot value: {%s: %s}",
key, key,
resolved_value, resolved_data["value"],
) )
return resolved_value return resolved_data
class AlexaResponse: class AlexaResponse:
@ -237,8 +247,10 @@ class AlexaResponse:
continue continue
_key = key.replace(".", "_") _key = key.replace(".", "_")
_slot_data = resolve_slot_data(key, value)
self.variables[_key] = resolve_slot_synonyms(key, value) self.variables[_key] = _slot_data["value"]
self.variables[_key + "_Id"] = _slot_data["id"]
def add_card(self, card_type, title, content): def add_card(self, card_type, title, content):
"""Add a card to the response.""" """Add a card to the response."""

View File

@ -77,6 +77,12 @@ def alexa_client(event_loop, hass, hass_client):
"text": "You told us your sign is {{ ZodiacSign }}.", "text": "You told us your sign is {{ ZodiacSign }}.",
} }
}, },
"GetZodiacHoroscopeIDIntent": {
"speech": {
"type": "plain",
"text": "You told us your sign is {{ ZodiacSign_Id }}.",
}
},
"AMAZON.PlaybackAction<object@MusicCreativeWork>": { "AMAZON.PlaybackAction<object@MusicCreativeWork>": {
"speech": { "speech": {
"type": "plain", "type": "plain",
@ -299,6 +305,113 @@ async def test_intent_request_with_slots_and_synonym_resolution(alexa_client) ->
assert text == "You told us your sign is Virgo." assert text == "You told us your sign is Virgo."
async def test_intent_request_with_slots_and_synonym_id_resolution(
alexa_client,
) -> None:
"""Test a request with slots, id and a name synonym."""
data = {
"version": "1.0",
"session": {
"new": False,
"sessionId": SESSION_ID,
"application": {"applicationId": APPLICATION_ID},
"attributes": {
"supportedHoroscopePeriods": {
"daily": True,
"weekly": False,
"monthly": False,
}
},
"user": {"userId": "amzn1.account.AM3B00000000000000000000000"},
},
"request": {
"type": "IntentRequest",
"requestId": REQUEST_ID,
"timestamp": "2015-05-13T12:34:56Z",
"intent": {
"name": "GetZodiacHoroscopeIDIntent",
"slots": {
"ZodiacSign": {
"name": "ZodiacSign",
"value": "V zodiac",
"resolutions": {
"resolutionsPerAuthority": [
{
"authority": AUTHORITY_ID,
"status": {"code": "ER_SUCCESS_MATCH"},
"values": [{"value": {"name": "Virgo", "id": "1"}}],
}
]
},
}
},
},
},
}
req = await _intent_req(alexa_client, data)
assert req.status == HTTPStatus.OK
data = await req.json()
text = data.get("response", {}).get("outputSpeech", {}).get("text")
assert text == "You told us your sign is 1."
async def test_intent_request_with_slots_and_multi_synonym_id_resolution(
alexa_client,
) -> None:
"""Test a request with slots and multiple name synonyms (id)."""
data = {
"version": "1.0",
"session": {
"new": False,
"sessionId": SESSION_ID,
"application": {"applicationId": APPLICATION_ID},
"attributes": {
"supportedHoroscopePeriods": {
"daily": True,
"weekly": False,
"monthly": False,
}
},
"user": {"userId": "amzn1.account.AM3B00000000000000000000000"},
},
"request": {
"type": "IntentRequest",
"requestId": REQUEST_ID,
"timestamp": "2015-05-13T12:34:56Z",
"intent": {
"name": "GetZodiacHoroscopeIDIntent",
"slots": {
"ZodiacSign": {
"name": "ZodiacSign",
"value": "Virgio Test",
"resolutions": {
"resolutionsPerAuthority": [
{
"authority": AUTHORITY_ID,
"status": {"code": "ER_SUCCESS_MATCH"},
"values": [
{"value": {"name": "Virgio Test", "id": "2"}}
],
},
{
"authority": AUTHORITY_ID,
"status": {"code": "ER_SUCCESS_MATCH"},
"values": [{"value": {"name": "Virgo", "id": "1"}}],
},
]
},
}
},
},
},
}
req = await _intent_req(alexa_client, data)
assert req.status == HTTPStatus.OK
data = await req.json()
text = data.get("response", {}).get("outputSpeech", {}).get("text")
assert text == "You told us your sign is 2."
async def test_intent_request_with_slots_and_multi_synonym_resolution( async def test_intent_request_with_slots_and_multi_synonym_resolution(
alexa_client, alexa_client,
) -> None: ) -> None: