Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 21 additions & 9 deletions homeassistant/components/alexa/intent.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Support for Alexa skill service end point."""
import enum
import logging
from typing import Any

from homeassistant.components import http
from homeassistant.core import callback
Expand Down Expand Up @@ -180,12 +181,15 @@ async def async_handle_intent(hass, message):
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."""
# 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:
# https://tinyurl.com/ybvm7jhs
resolved_value = request["value"]
resolved_data = {}
resolved_data["value"] = request["value"]
resolved_data["id"] = ""

if (
"resolutions" in request
Expand All @@ -200,20 +204,26 @@ def resolve_slot_synonyms(key, request):
if entry["status"]["code"] != SYN_RESOLUTION_MATCH:
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
# 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:
resolved_value = possible_values[0]
resolved_data["value"] = possible_values[0]["name"]
else:
_LOGGER.debug(
"Found multiple synonym resolutions for slot value: {%s: %s}",
key,
resolved_value,
resolved_data["value"],
)

return resolved_value
return resolved_data


class AlexaResponse:
Expand All @@ -237,8 +247,10 @@ def __init__(self, hass, intent_info):
continue

_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):
"""Add a card to the response."""
Expand Down
113 changes: 113 additions & 0 deletions tests/components/alexa/test_intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ def mock_service(call):
"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>": {
"speech": {
"type": "plain",
Expand Down Expand Up @@ -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."


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(
alexa_client,
) -> None:
Expand Down