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_MAIN_TEXT = 'mainText'
ATTR_REDIRECTION_URL = 'redirectionURL' ATTR_REDIRECTION_URL = 'redirectionURL'
SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH'
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z' 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 For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alexa/ https://home-assistant.io/components/alexa/
""" """
import asyncio import asyncio
import enum import enum
@ -13,7 +14,7 @@ from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import intent from homeassistant.helpers import intent
from homeassistant.components import http from homeassistant.components import http
from .const import DOMAIN from .const import DOMAIN, SYN_RESOLUTION_MATCH
INTENTS_API_ENDPOINT = '/api/alexa' INTENTS_API_ENDPOINT = '/api/alexa'
@ -123,6 +124,43 @@ class AlexaIntentsView(http.HomeAssistantView):
return self.json(alexa_response) 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): class AlexaResponse(object):
"""Help generating the response for Alexa.""" """Help generating the response for Alexa."""
@ -135,28 +173,17 @@ class AlexaResponse(object):
self.session_attributes = {} self.session_attributes = {}
self.should_end_session = True self.should_end_session = True
self.variables = {} self.variables = {}
# Intent is None if request was a LaunchRequest or SessionEndedRequest # Intent is None if request was a LaunchRequest or SessionEndedRequest
if intent_info is not None: if intent_info is not None:
for key, value in intent_info.get('slots', {}).items(): for key, value in intent_info.get('slots', {}).items():
underscored_key = key.replace('.', '_') # Only include slots with values
if 'value' not in value:
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 continue
for resolved in resolution['values']: _key = key.replace('.', '_')
if 'value' not in resolved:
continue
if 'name' in resolved['value']: self.variables[_key] = resolve_slot_synonyms(key, value)
self.variables[underscored_key] = resolved['value']['name']
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

@ -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" APPLICATION_ID = "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe"
REQUEST_ID = "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000" REQUEST_ID = "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000"
AUTHORITY_ID = "amzn1.er-authority.000000-d0ed-0000-ad00-000000d00ebe.ZODIAC" 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 # pylint: disable=invalid-name
calls = [] calls = []
@ -209,7 +210,7 @@ def test_intent_request_with_slots(alexa_client):
@asyncio.coroutine @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.""" """Test a request with slots and a name synonym."""
data = { data = {
"version": "1.0", "version": "1.0",
@ -239,7 +240,7 @@ def test_intent_request_with_slots_and_name_resolution(alexa_client):
"slots": { "slots": {
"ZodiacSign": { "ZodiacSign": {
"name": "ZodiacSign", "name": "ZodiacSign",
"value": "virgo", "value": "V zodiac",
"resolutions": { "resolutions": {
"resolutionsPerAuthority": [ "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." 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 @asyncio.coroutine
def test_intent_request_with_slots_but_no_value(alexa_client): def test_intent_request_with_slots_but_no_value(alexa_client):
"""Test a request with slots but no value.""" """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", "name": "GetZodiacHoroscopeIntent",
"slots": { "slots": {
"ZodiacSign": { "ZodiacSign": {
"name": "ZodiacSign", "name": "ZodiacSign"
} }
} }
} }