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 3 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'
72 changes: 53 additions & 19 deletions homeassistant/components/alexa/intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@

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

Notes:
[1] - https://developer.amazon.com/docs/custom-skills/define-synonyms-and-ids-for-slot-type-values-entity-resolution.html#intentrequest-changes

""" # noqa
import asyncio
import enum
import logging
import itertools

from homeassistant.core import callback
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'

Expand Down Expand Up @@ -123,6 +128,46 @@ 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 [1]
resolved_value = request['value']

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

# Only consider the authorities with a successful match
matches = filter(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not use filter but instead use list comprehensions

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'])

lambda x: x['status']['code'] == SYN_RESOLUTION_MATCH,
request['resolutions']['resolutionsPerAuthority']
)

# Extract all of the possible values from the object.
possible_values = list(
itertools.chain.from_iterable([
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please write idiomatic Python using for loops and appending to lists.

map(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not use map but instead use list comprehensions

lambda val: val['value']['name'],
x['values']
) for x in matches
])
)

# 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 +180,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