Skip to content

Commit

Permalink
Update intent response (home-assistant#83858)
Browse files Browse the repository at this point in the history
* Add language to conversation and intent response

* Move language to intent response instead of speech

* Extend intent response for voice MVP

* Add tests for error conditions in conversation/process

* Move intent response type data into "data" field

* Move intent response error message back to speech

* Remove "success" from intent response

* Add id to target in intent response

* target defaults to None

* Update homeassistant/helpers/intent.py

* Fix test

* Return conversation_id and multiple targets

* Clean up git mess

* Fix linting errors

* Fix more async_handle signatures

* Separate conversation_id and IntentResponse

* Add unknown error code

* Add ConversationResult

* Don't set domain on single entity

* Language is required for intent response

* Add partial_action_done

* Default language in almond agent

Co-authored-by: Paulus Schoutsen <[email protected]>
  • Loading branch information
synesthesiam and balloob authored Dec 13, 2022
1 parent 0e2ebfe commit 961c8cc
Show file tree
Hide file tree
Showing 12 changed files with 300 additions and 140 deletions.
11 changes: 7 additions & 4 deletions homeassistant/components/almond/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,10 @@ async def async_process(
context: Context,
conversation_id: str | None = None,
language: str | None = None,
) -> intent.IntentResponse:
) -> conversation.ConversationResult | None:
"""Process a sentence."""
response = await self.api.async_converse_text(text, conversation_id)
language = language or self.hass.config.language

first_choice = True
buffer = ""
Expand All @@ -314,6 +315,8 @@ async def async_process(
buffer += ","
buffer += f" {message['title']}"

intent_result = intent.IntentResponse(language=language)
intent_result.async_set_speech(buffer.strip())
return intent_result
intent_response = intent.IntentResponse(language=language)
intent_response.async_set_speech(buffer.strip())
return conversation.ConversationResult(
response=intent_response, conversation_id=conversation_id
)
92 changes: 44 additions & 48 deletions homeassistant/components/conversation/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Support for functionality to have conversations with Home Assistant."""
from __future__ import annotations

from http import HTTPStatus
import logging
import re
from typing import Any
Expand All @@ -16,7 +15,7 @@
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass

from .agent import AbstractConversationAgent
from .agent import AbstractConversationAgent, ConversationResult
from .default_agent import DefaultAgent, async_register

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -101,16 +100,14 @@ async def websocket_process(
msg: dict[str, Any],
) -> None:
"""Process text."""
connection.send_result(
msg["id"],
await _async_converse(
hass,
msg["text"],
msg.get("conversation_id"),
connection.context(msg),
msg.get("language"),
),
result = await _async_converse(
hass,
msg["text"],
msg.get("conversation_id"),
connection.context(msg),
msg.get("language"),
)
connection.send_result(msg["id"], result.as_dict())


@websocket_api.websocket_command({"type": "conversation/agent/info"})
Expand Down Expand Up @@ -168,29 +165,15 @@ class ConversationProcessView(http.HomeAssistantView):
async def post(self, request, data):
"""Send a request for processing."""
hass = request.app["hass"]
result = await _async_converse(
hass,
text=data["text"],
conversation_id=data.get("conversation_id"),
context=self.context(request),
language=data.get("language"),
)

try:
intent_result = await _async_converse(
hass,
text=data["text"],
conversation_id=data.get("conversation_id"),
context=self.context(request),
language=data.get("language"),
)
except intent.IntentError as err:
_LOGGER.error("Error handling intent: %s", err)
return self.json(
{
"success": False,
"error": {
"code": str(err.__class__.__name__).lower(),
"message": str(err),
},
},
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
)

return self.json(intent_result)
return self.json(result.as_dict())


async def _get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent:
Expand All @@ -207,37 +190,50 @@ async def _async_converse(
conversation_id: str | None,
context: core.Context,
language: str | None = None,
) -> intent.IntentResponse:
) -> ConversationResult:
"""Process text and get intent."""
agent = await _get_agent(hass)
if language is None:
language = hass.config.language

result: ConversationResult | None = None
intent_response: intent.IntentResponse | None = None

try:
intent_result = await agent.async_process(
text, context, conversation_id, language
)
result = await agent.async_process(text, context, conversation_id, language)
except intent.IntentHandleError as err:
# Match was successful, but target(s) were invalid
intent_result = intent.IntentResponse(language=language)
intent_result.async_set_error(
intent_response = intent.IntentResponse(language=language)
intent_response.async_set_error(
intent.IntentResponseErrorCode.NO_VALID_TARGETS,
str(err),
)
except intent.IntentUnexpectedError as err:
# Match was successful, but an error occurred while handling intent
intent_result = intent.IntentResponse(language=language)
intent_result.async_set_error(
intent_response = intent.IntentResponse(language=language)
intent_response.async_set_error(
intent.IntentResponseErrorCode.FAILED_TO_HANDLE,
str(err),
)
except intent.IntentError as err:
# Unknown error
intent_response = intent.IntentResponse(language=language)
intent_response.async_set_error(
intent.IntentResponseErrorCode.UNKNOWN,
str(err),
)

if result is None:
if intent_response is None:
# Match was not successful
intent_response = intent.IntentResponse(language=language)
intent_response.async_set_error(
intent.IntentResponseErrorCode.NO_INTENT_MATCH,
"Sorry, I didn't understand that",
)

if intent_result is None:
# Match was not successful
intent_result = intent.IntentResponse(language=language)
intent_result.async_set_error(
intent.IntentResponseErrorCode.NO_INTENT_MATCH,
"Sorry, I didn't understand that",
result = ConversationResult(
response=intent_response, conversation_id=conversation_id
)

return intent_result
return result
19 changes: 18 additions & 1 deletion homeassistant/components/conversation/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,28 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any

from homeassistant.core import Context
from homeassistant.helpers import intent


@dataclass
class ConversationResult:
"""Result of async_process."""

response: intent.IntentResponse
conversation_id: str | None = None

def as_dict(self) -> dict[str, Any]:
"""Return result as a dict."""
return {
"response": self.response.as_dict(),
"conversation_id": self.conversation_id,
}


class AbstractConversationAgent(ABC):
"""Abstract conversation agent."""

Expand All @@ -30,5 +47,5 @@ async def async_process(
context: Context,
conversation_id: str | None = None,
language: str | None = None,
) -> intent.IntentResponse | None:
) -> ConversationResult | None:
"""Process a sentence."""
10 changes: 7 additions & 3 deletions homeassistant/components/conversation/default_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from homeassistant.helpers import intent
from homeassistant.setup import ATTR_COMPONENT

from .agent import AbstractConversationAgent
from .agent import AbstractConversationAgent, ConversationResult
from .const import DOMAIN
from .util import create_matcher

Expand Down Expand Up @@ -116,7 +116,7 @@ async def async_process(
context: core.Context,
conversation_id: str | None = None,
language: str | None = None,
) -> intent.IntentResponse | None:
) -> ConversationResult | None:
"""Process a sentence."""
intents = self.hass.data[DOMAIN]

Expand All @@ -125,7 +125,7 @@ async def async_process(
if not (match := matcher.match(text)):
continue

return await intent.async_handle(
intent_response = await intent.async_handle(
self.hass,
DOMAIN,
intent_type,
Expand All @@ -135,4 +135,8 @@ async def async_process(
language,
)

return ConversationResult(
response=intent_response, conversation_id=conversation_id
)

return None
2 changes: 2 additions & 0 deletions homeassistant/components/humidifier/intent.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Intents for the humidifier integration."""
from __future__ import annotations

import voluptuous as vol

from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, STATE_OFF
Expand Down
5 changes: 3 additions & 2 deletions homeassistant/components/intent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class IntentHandleView(http.HomeAssistantView):
async def post(self, request, data):
"""Handle intent with name/data."""
hass = request.app["hass"]
language = hass.config.language

try:
intent_name = data["name"]
Expand All @@ -73,11 +74,11 @@ async def post(self, request, data):
hass, DOMAIN, intent_name, slots, "", self.context(request)
)
except intent.IntentHandleError as err:
intent_result = intent.IntentResponse()
intent_result = intent.IntentResponse(language=language)
intent_result.async_set_speech(str(err))

if intent_result is None:
intent_result = intent.IntentResponse()
intent_result = intent.IntentResponse(language=language)
intent_result.async_set_speech("Sorry, I couldn't handle that")

return self.json(intent_result)
4 changes: 3 additions & 1 deletion homeassistant/components/intent_script/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Handle intents with scripts."""
from __future__ import annotations

import copy
import logging

Expand Down Expand Up @@ -77,7 +79,7 @@ def __init__(self, intent_type, config):
self.intent_type = intent_type
self.config = config

async def async_handle(self, intent_obj):
async def async_handle(self, intent_obj: intent.Intent):
"""Handle the intent."""
speech = self.config.get(CONF_SPEECH)
reprompt = self.config.get(CONF_REPROMPT)
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/light/intent.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Intents for the light integration."""
from __future__ import annotations

import voluptuous as vol

from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON
Expand Down
6 changes: 4 additions & 2 deletions homeassistant/components/shopping_list/intent.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Intents for the Shopping List integration."""
from __future__ import annotations

from homeassistant.helpers import intent
import homeassistant.helpers.config_validation as cv

Expand All @@ -20,7 +22,7 @@ class AddItemIntent(intent.IntentHandler):
intent_type = INTENT_ADD_ITEM
slot_schema = {"item": cv.string}

async def async_handle(self, intent_obj):
async def async_handle(self, intent_obj: intent.Intent):
"""Handle the intent."""
slots = self.async_validate_slots(intent_obj.slots)
item = slots["item"]["value"]
Expand All @@ -38,7 +40,7 @@ class ListTopItemsIntent(intent.IntentHandler):
intent_type = INTENT_LAST_ITEMS
slot_schema = {"item": cv.string}

async def async_handle(self, intent_obj):
async def async_handle(self, intent_obj: intent.Intent):
"""Handle the intent."""
items = intent_obj.hass.data[DOMAIN].items[-5:]
response = intent_obj.create_response()
Expand Down
Loading

0 comments on commit 961c8cc

Please sign in to comment.