Alexa slot synonym fix (#10614)

* Added logic to the alexa component for handling slot synonyms

* Moved note with long url to the top of the file

* Just made a tiny url instead of messing with Flake8

* Refactored to be more Pythonic

* Put trailing comma back
This commit is contained in:
Corey Pauley 2017-11-16 23:09:00 -06:00 committed by Paulus Schoutsen
parent aa6b37912a
commit 6cf2e758a8
3 changed files with 139 additions and 21 deletions

View File

@ -15,4 +15,6 @@ ATTR_STREAM_URL = 'streamUrl'
ATTR_MAIN_TEXT = 'mainText'
ATTR_REDIRECTION_URL = 'redirectionURL'
SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH'
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'

View File

@ -3,6 +3,7 @@ Support for Alexa skill service end point.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alexa/
"""
import asyncio
import enum
@ -13,7 +14,7 @@ from homeassistant.const import HTTP_BAD_REQUEST
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'
@ -123,6 +124,43 @@ class AlexaIntentsView(http.HomeAssistantView):
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."""
@ -135,28 +173,17 @@ class AlexaResponse(object):
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."""

View File

@ -14,6 +14,7 @@ SESSION_ID = "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000"
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 = []
@ -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",
@ -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": [
{
@ -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"
}
}
]
}
]
}
@ -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."""
@ -300,7 +389,7 @@ def test_intent_request_with_slots_but_no_value(alexa_client):
"name": "GetZodiacHoroscopeIntent",
"slots": {
"ZodiacSign": {
"name": "ZodiacSign",
"name": "ZodiacSign"
}
}
}