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 slot synonym fix #10614

Merged
merged 6 commits into from
Nov 17, 2017
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions homeassistant/components/alexa/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@
ATTR_MAIN_TEXT = 'mainText'
ATTR_REDIRECTION_URL = 'redirectionURL'

SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH'

DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
63 changes: 45 additions & 18 deletions homeassistant/components/alexa/intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alexa/

"""
import asyncio
import enum
Expand All @@ -13,7 +14,7 @@
from homeassistant.helpers import intent
from homeassistant.components import http

from .const import DOMAIN
from .const import DOMAIN, SYN_RESOLUTION_MATCH

INTENTS_API_ENDPOINT = '/api/alexa'

Expand Down Expand Up @@ -123,6 +124,43 @@ def post(self, request):
return self.json(alexa_response)


def resolve_slot_synonyms(key, request):
"""Check slot request for synonym resolutions."""
# Default to the spoken slot value if more than one or none are found. For
# reference to the request object structure, see the Alexa docs:
# https://tinyurl.com/ybvm7jhs
resolved_value = request['value']

if ('resolutions' in request and
'resolutionsPerAuthority' in request['resolutions'] and
len(request['resolutions']['resolutionsPerAuthority']) >= 1):

# Extract all of the possible values from each authority with a
# successful match
possible_values = []

for entry in request['resolutions']['resolutionsPerAuthority']:
if entry['status']['code'] != SYN_RESOLUTION_MATCH:
continue

possible_values.extend([item['value']['name']
for item
in entry['values']])

# If there is only one match use the resolved value, otherwise the
# resolution cannot be determined, so use the spoken slot value
if len(possible_values) == 1:
resolved_value = possible_values[0]
else:
_LOGGER.debug(
'Found multiple synonym resolutions for slot value: {%s: %s}',
key,
request['value']
)

return resolved_value


class AlexaResponse(object):
"""Help generating the response for Alexa."""

Expand All @@ -135,28 +173,17 @@ def __init__(self, hass, intent_info):
self.session_attributes = {}
self.should_end_session = True
self.variables = {}

# Intent is None if request was a LaunchRequest or SessionEndedRequest
if intent_info is not None:
for key, value in intent_info.get('slots', {}).items():
underscored_key = key.replace('.', '_')

if 'value' in value:
self.variables[underscored_key] = value['value']

if 'resolutions' in value:
self._populate_resolved_values(underscored_key, value)

def _populate_resolved_values(self, underscored_key, value):
for resolution in value['resolutions']['resolutionsPerAuthority']:
if 'values' not in resolution:
continue

for resolved in resolution['values']:
if 'value' not in resolved:
# Only include slots with values
if 'value' not in value:
continue

if 'name' in resolved['value']:
self.variables[underscored_key] = resolved['value']['name']
_key = key.replace('.', '_')

self.variables[_key] = resolve_slot_synonyms(key, value)

def add_card(self, card_type, title, content):
"""Add a card to the response."""
Expand Down
95 changes: 92 additions & 3 deletions tests/components/alexa/test_intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
APPLICATION_ID = "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe"
REQUEST_ID = "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000"
AUTHORITY_ID = "amzn1.er-authority.000000-d0ed-0000-ad00-000000d00ebe.ZODIAC"
BUILTIN_AUTH_ID = "amzn1.er-authority.000000-d0ed-0000-ad00-000000d00ebe.TEST"

# pylint: disable=invalid-name
calls = []
Expand Down Expand Up @@ -209,7 +210,7 @@ def test_intent_request_with_slots(alexa_client):


@asyncio.coroutine
def test_intent_request_with_slots_and_name_resolution(alexa_client):
def test_intent_request_with_slots_and_synonym_resolution(alexa_client):
"""Test a request with slots and a name synonym."""
data = {
"version": "1.0",
Expand Down Expand Up @@ -239,7 +240,7 @@ def test_intent_request_with_slots_and_name_resolution(alexa_client):
"slots": {
"ZodiacSign": {
"name": "ZodiacSign",
"value": "virgo",
"value": "V zodiac",
"resolutions": {
"resolutionsPerAuthority": [
{
Expand All @@ -254,6 +255,19 @@ def test_intent_request_with_slots_and_name_resolution(alexa_client):
}
}
]
},
{
"authority": BUILTIN_AUTH_ID,
"status": {
"code": "ER_SUCCESS_NO_MATCH"
},
"values": [
{
"value": {
"name": "Test"
}
}
]
}
]
}
Expand All @@ -270,6 +284,81 @@ def test_intent_request_with_slots_and_name_resolution(alexa_client):
assert text == "You told us your sign is Virgo."


@asyncio.coroutine
def test_intent_request_with_slots_and_multi_synonym_resolution(alexa_client):
"""Test a request with slots and multiple name synonyms."""
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": "GetZodiacHoroscopeIntent",
"slots": {
"ZodiacSign": {
"name": "ZodiacSign",
"value": "V zodiac",
"resolutions": {
"resolutionsPerAuthority": [
{
"authority": AUTHORITY_ID,
"status": {
"code": "ER_SUCCESS_MATCH"
},
"values": [
{
"value": {
"name": "Virgo"
}
}
]
},
{
"authority": BUILTIN_AUTH_ID,
"status": {
"code": "ER_SUCCESS_MATCH"
},
"values": [
{
"value": {
"name": "Test"
}
}
]
}
]
}
}
}
}
}
}
req = yield from _intent_req(alexa_client, data)
assert req.status == 200
data = yield from req.json()
text = data.get("response", {}).get("outputSpeech",
{}).get("text")
assert text == "You told us your sign is V zodiac."


@asyncio.coroutine
def test_intent_request_with_slots_but_no_value(alexa_client):
"""Test a request with slots but no value."""
Expand Down Expand Up @@ -300,7 +389,7 @@ def test_intent_request_with_slots_but_no_value(alexa_client):
"name": "GetZodiacHoroscopeIntent",
"slots": {
"ZodiacSign": {
"name": "ZodiacSign",
"name": "ZodiacSign"
}
}
}
Expand Down