mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 17:27:52 +00:00
Add optional words to conversation utterances (#12772)
* Add optional words to conversation utterances * Conversation to handle singular/plural * Remove print * Add pronounce detection to shopping list * Lint * fix tests * Add optional 2 words * Fix tests * Conversation: coroutine -> async/await * Replace \s with space
This commit is contained in:
parent
7d8ca2010b
commit
491b3d707c
@ -156,9 +156,10 @@ def async_setup(hass, config):
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned on {}"))
|
||||
intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on"))
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF, "Turned off {}"))
|
||||
intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF,
|
||||
"Turned {} off"))
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"))
|
||||
|
||||
|
@ -4,7 +4,6 @@ Support for functionality to have conversations with Home Assistant.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/conversation/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
@ -67,8 +66,7 @@ def async_register(hass, intent_type, utterances):
|
||||
conf.append(_create_matcher(utterance))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Register the process service."""
|
||||
config = config.get(DOMAIN, {})
|
||||
intents = hass.data.get(DOMAIN)
|
||||
@ -84,49 +82,73 @@ def async_setup(hass, config):
|
||||
|
||||
conf.extend(_create_matcher(utterance) for utterance in utterances)
|
||||
|
||||
@asyncio.coroutine
|
||||
def process(service):
|
||||
async def process(service):
|
||||
"""Parse text into commands."""
|
||||
text = service.data[ATTR_TEXT]
|
||||
yield from _process(hass, text)
|
||||
try:
|
||||
await _process(hass, text)
|
||||
except intent.IntentHandleError as err:
|
||||
_LOGGER.error('Error processing %s: %s', text, err)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA)
|
||||
|
||||
hass.http.register_view(ConversationProcessView)
|
||||
|
||||
async_register(hass, intent.INTENT_TURN_ON,
|
||||
['Turn {name} on', 'Turn on {name}'])
|
||||
async_register(hass, intent.INTENT_TURN_OFF,
|
||||
['Turn {name} off', 'Turn off {name}'])
|
||||
async_register(hass, intent.INTENT_TOGGLE,
|
||||
['Toggle {name}', '{name} toggle'])
|
||||
# We strip trailing 's' from name because our state matcher will fail
|
||||
# if a letter is not there. By removing 's' we can match singular and
|
||||
# plural names.
|
||||
|
||||
async_register(hass, intent.INTENT_TURN_ON, [
|
||||
'Turn [the] [a] {name}[s] on',
|
||||
'Turn on [the] [a] [an] {name}[s]',
|
||||
])
|
||||
async_register(hass, intent.INTENT_TURN_OFF, [
|
||||
'Turn [the] [a] [an] {name}[s] off',
|
||||
'Turn off [the] [a] [an] {name}[s]',
|
||||
])
|
||||
async_register(hass, intent.INTENT_TOGGLE, [
|
||||
'Toggle [the] [a] [an] {name}[s]',
|
||||
'[the] [a] [an] {name}[s] toggle',
|
||||
])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _create_matcher(utterance):
|
||||
"""Create a regex that matches the utterance."""
|
||||
parts = re.split(r'({\w+})', utterance)
|
||||
# Split utterance into parts that are type: NORMAL, GROUP or OPTIONAL
|
||||
# Pattern matches (GROUP|OPTIONAL): Change light to [the color] {name}
|
||||
parts = re.split(r'({\w+}|\[[\w\s]+\] *)', utterance)
|
||||
# Pattern to extract name from GROUP part. Matches {name}
|
||||
group_matcher = re.compile(r'{(\w+)}')
|
||||
# Pattern to extract text from OPTIONAL part. Matches [the color]
|
||||
optional_matcher = re.compile(r'\[([\w ]+)\] *')
|
||||
|
||||
pattern = ['^']
|
||||
|
||||
for part in parts:
|
||||
match = group_matcher.match(part)
|
||||
group_match = group_matcher.match(part)
|
||||
optional_match = optional_matcher.match(part)
|
||||
|
||||
if match is None:
|
||||
# Normal part
|
||||
if group_match is None and optional_match is None:
|
||||
pattern.append(part)
|
||||
continue
|
||||
|
||||
pattern.append('(?P<{}>{})'.format(match.groups()[0], r'[\w ]+'))
|
||||
# Group part
|
||||
if group_match is not None:
|
||||
pattern.append(
|
||||
r'(?P<{}>[\w ]+?)\s*'.format(group_match.groups()[0]))
|
||||
|
||||
# Optional part
|
||||
elif optional_match is not None:
|
||||
pattern.append(r'(?:{} *)?'.format(optional_match.groups()[0]))
|
||||
|
||||
pattern.append('$')
|
||||
return re.compile(''.join(pattern), re.I)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _process(hass, text):
|
||||
async def _process(hass, text):
|
||||
"""Process a line of text."""
|
||||
intents = hass.data.get(DOMAIN, {})
|
||||
|
||||
@ -137,7 +159,7 @@ def _process(hass, text):
|
||||
if not match:
|
||||
continue
|
||||
|
||||
response = yield from hass.helpers.intent.async_handle(
|
||||
response = await hass.helpers.intent.async_handle(
|
||||
DOMAIN, intent_type,
|
||||
{key: {'value': value} for key, value
|
||||
in match.groupdict().items()}, text)
|
||||
@ -153,12 +175,15 @@ class ConversationProcessView(http.HomeAssistantView):
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('text'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
async def post(self, request, data):
|
||||
"""Send a request for processing."""
|
||||
hass = request.app['hass']
|
||||
|
||||
intent_result = yield from _process(hass, data['text'])
|
||||
try:
|
||||
intent_result = await _process(hass, data['text'])
|
||||
except intent.IntentHandleError as err:
|
||||
intent_result = intent.IntentResponse()
|
||||
intent_result.async_set_speech(str(err))
|
||||
|
||||
if intent_result is None:
|
||||
intent_result = intent.IntentResponse()
|
||||
|
@ -43,7 +43,7 @@ def async_setup(hass, config):
|
||||
hass.http.register_view(ClearCompletedItemsView)
|
||||
|
||||
hass.components.conversation.async_register(INTENT_ADD_ITEM, [
|
||||
'Add {item} to my shopping list',
|
||||
'Add [the] [a] [an] {item} to my shopping list',
|
||||
])
|
||||
hass.components.conversation.async_register(INTENT_LAST_ITEMS, [
|
||||
'What is on my shopping list'
|
||||
|
@ -100,7 +100,8 @@ def async_match_state(hass, name, states=None):
|
||||
state = _fuzzymatch(name, states, lambda state: state.name)
|
||||
|
||||
if state is None:
|
||||
raise IntentHandleError('Unable to find entity {}'.format(name))
|
||||
raise IntentHandleError(
|
||||
'Unable to find an entity called {}'.format(name))
|
||||
|
||||
return state
|
||||
|
||||
|
@ -237,7 +237,7 @@ def test_http_api(hass, test_client):
|
||||
calls = async_mock_service(hass, 'homeassistant', 'turn_on')
|
||||
|
||||
resp = yield from client.post('/api/conversation/process', json={
|
||||
'text': 'Turn kitchen on'
|
||||
'text': 'Turn the kitchen on'
|
||||
})
|
||||
assert resp.status == 200
|
||||
|
||||
@ -267,3 +267,56 @@ def test_http_api_wrong_data(hass, test_client):
|
||||
resp = yield from client.post('/api/conversation/process', json={
|
||||
})
|
||||
assert resp.status == 400
|
||||
|
||||
|
||||
def test_create_matcher():
|
||||
"""Test the create matcher method."""
|
||||
# Basic sentence
|
||||
pattern = conversation._create_matcher('Hello world')
|
||||
assert pattern.match('Hello world') is not None
|
||||
|
||||
# Match a part
|
||||
pattern = conversation._create_matcher('Hello {name}')
|
||||
match = pattern.match('hello world')
|
||||
assert match is not None
|
||||
assert match.groupdict()['name'] == 'world'
|
||||
no_match = pattern.match('Hello world, how are you?')
|
||||
assert no_match is None
|
||||
|
||||
# Optional and matching part
|
||||
pattern = conversation._create_matcher('Turn on [the] {name}')
|
||||
match = pattern.match('turn on the kitchen lights')
|
||||
assert match is not None
|
||||
assert match.groupdict()['name'] == 'kitchen lights'
|
||||
match = pattern.match('turn on kitchen lights')
|
||||
assert match is not None
|
||||
assert match.groupdict()['name'] == 'kitchen lights'
|
||||
match = pattern.match('turn off kitchen lights')
|
||||
assert match is None
|
||||
|
||||
# Two different optional parts, 1 matching part
|
||||
pattern = conversation._create_matcher('Turn on [the] [a] {name}')
|
||||
match = pattern.match('turn on the kitchen lights')
|
||||
assert match is not None
|
||||
assert match.groupdict()['name'] == 'kitchen lights'
|
||||
match = pattern.match('turn on kitchen lights')
|
||||
assert match is not None
|
||||
assert match.groupdict()['name'] == 'kitchen lights'
|
||||
match = pattern.match('turn on a kitchen light')
|
||||
assert match is not None
|
||||
assert match.groupdict()['name'] == 'kitchen light'
|
||||
|
||||
# Strip plural
|
||||
pattern = conversation._create_matcher('Turn {name}[s] on')
|
||||
match = pattern.match('turn kitchen lights on')
|
||||
assert match is not None
|
||||
assert match.groupdict()['name'] == 'kitchen light'
|
||||
|
||||
# Optional 2 words
|
||||
pattern = conversation._create_matcher('Turn [the great] {name} on')
|
||||
match = pattern.match('turn the great kitchen lights on')
|
||||
assert match is not None
|
||||
assert match.groupdict()['name'] == 'kitchen lights'
|
||||
match = pattern.match('turn kitchen lights on')
|
||||
assert match is not None
|
||||
assert match.groupdict()['name'] == 'kitchen lights'
|
||||
|
@ -212,7 +212,7 @@ def test_turn_on_intent(hass):
|
||||
)
|
||||
yield from hass.async_block_till_done()
|
||||
|
||||
assert response.speech['plain']['speech'] == 'Turned on test light'
|
||||
assert response.speech['plain']['speech'] == 'Turned test light on'
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
assert call.domain == 'light'
|
||||
@ -234,7 +234,7 @@ def test_turn_off_intent(hass):
|
||||
)
|
||||
yield from hass.async_block_till_done()
|
||||
|
||||
assert response.speech['plain']['speech'] == 'Turned off test light'
|
||||
assert response.speech['plain']['speech'] == 'Turned test light off'
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
assert call.domain == 'light'
|
||||
@ -283,7 +283,7 @@ def test_turn_on_multiple_intent(hass):
|
||||
)
|
||||
yield from hass.async_block_till_done()
|
||||
|
||||
assert response.speech['plain']['speech'] == 'Turned on test lights 2'
|
||||
assert response.speech['plain']['speech'] == 'Turned test lights 2 on'
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
assert call.domain == 'light'
|
||||
|
Loading…
x
Reference in New Issue
Block a user