Add language to conversation and intent response (#83486)

* Add language to conversation and intent response

* Add language parameter to conversation/process service

* Move language to intent response instead of speech

* Add language to almond conversation agent

* Fix intent test
This commit is contained in:
Michael Hansen 2022-12-08 10:39:28 -06:00 committed by GitHub
parent ee8a2d1e25
commit ac87528bec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 133 additions and 34 deletions

View File

@ -286,7 +286,11 @@ class AlmondAgent(conversation.AbstractConversationAgent):
return True
async def async_process(
self, text: str, context: Context, conversation_id: str | None = None
self,
text: str,
context: Context,
conversation_id: str | None = None,
language: str | None = None,
) -> intent.IntentResponse:
"""Process a sentence."""
response = await self.api.async_converse_text(text, conversation_id)
@ -310,6 +314,6 @@ class AlmondAgent(conversation.AbstractConversationAgent):
buffer += ","
buffer += f" {message['title']}"
intent_result = intent.IntentResponse()
intent_result = intent.IntentResponse(language=language)
intent_result.async_set_speech(buffer.strip())
return intent_result

View File

@ -22,6 +22,7 @@ from .default_agent import DefaultAgent, async_register
_LOGGER = logging.getLogger(__name__)
ATTR_TEXT = "text"
ATTR_LANGUAGE = "language"
DOMAIN = "conversation"
@ -31,7 +32,9 @@ DATA_CONFIG = "conversation_config"
SERVICE_PROCESS = "process"
SERVICE_PROCESS_SCHEMA = vol.Schema({vol.Required(ATTR_TEXT): cv.string})
SERVICE_PROCESS_SCHEMA = vol.Schema(
{vol.Required(ATTR_TEXT): cv.string, vol.Optional(ATTR_LANGUAGE): cv.string}
)
CONFIG_SCHEMA = vol.Schema(
{
@ -66,7 +69,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
_LOGGER.debug("Processing: <%s>", text)
agent = await _get_agent(hass)
try:
await agent.async_process(text, service.context)
await agent.async_process(
text, service.context, language=service.data.get(ATTR_LANGUAGE)
)
except intent.IntentHandleError as err:
_LOGGER.error("Error processing %s: %s", text, err)
@ -82,7 +87,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@websocket_api.websocket_command(
{"type": "conversation/process", "text": str, vol.Optional("conversation_id"): str}
{
"type": "conversation/process",
"text": str,
vol.Optional("conversation_id"): str,
vol.Optional("language"): str,
}
)
@websocket_api.async_response
async def websocket_process(
@ -94,7 +104,11 @@ async def websocket_process(
connection.send_result(
msg["id"],
await _async_converse(
hass, msg["text"], msg.get("conversation_id"), connection.context(msg)
hass,
msg["text"],
msg.get("conversation_id"),
connection.context(msg),
msg.get("language"),
),
)
@ -143,7 +157,13 @@ class ConversationProcessView(http.HomeAssistantView):
name = "api:conversation:process"
@RequestDataValidator(
vol.Schema({vol.Required("text"): str, vol.Optional("conversation_id"): str})
vol.Schema(
{
vol.Required("text"): str,
vol.Optional("conversation_id"): str,
vol.Optional("language"): str,
}
)
)
async def post(self, request, data):
"""Send a request for processing."""
@ -151,7 +171,11 @@ class ConversationProcessView(http.HomeAssistantView):
try:
intent_result = await _async_converse(
hass, data["text"], data.get("conversation_id"), self.context(request)
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)
@ -182,17 +206,20 @@ async def _async_converse(
text: str,
conversation_id: str | None,
context: core.Context,
language: str | None = None,
) -> intent.IntentResponse:
"""Process text and get intent."""
agent = await _get_agent(hass)
try:
intent_result = await agent.async_process(text, context, conversation_id)
intent_result = await agent.async_process(
text, context, conversation_id, language
)
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 didn't understand that")
return intent_result

View File

@ -25,6 +25,10 @@ class AbstractConversationAgent(ABC):
@abstractmethod
async def async_process(
self, text: str, context: Context, conversation_id: str | None = None
self,
text: str,
context: Context,
conversation_id: str | None = None,
language: str | None = None,
) -> intent.IntentResponse | None:
"""Process a sentence."""

View File

@ -111,7 +111,11 @@ class DefaultAgent(AbstractConversationAgent):
async_register(self.hass, intent_type, sentences)
async def async_process(
self, text: str, context: core.Context, conversation_id: str | None = None
self,
text: str,
context: core.Context,
conversation_id: str | None = None,
language: str | None = None,
) -> intent.IntentResponse | None:
"""Process a sentence."""
intents = self.hass.data[DOMAIN]
@ -128,6 +132,7 @@ class DefaultAgent(AbstractConversationAgent):
{key: {"value": value} for key, value in match.groupdict().items()},
text,
context,
language,
)
return None

View File

@ -56,6 +56,7 @@ async def async_handle(
slots: _SlotsType | None = None,
text_input: str | None = None,
context: Context | None = None,
language: str | None = None,
) -> IntentResponse:
"""Handle an intent."""
handler: IntentHandler = hass.data.get(DATA_KEY, {}).get(intent_type)
@ -66,7 +67,12 @@ async def async_handle(
if context is None:
context = Context()
intent = Intent(hass, platform, intent_type, slots or {}, text_input, context)
if language is None:
language = hass.config.language
intent = Intent(
hass, platform, intent_type, slots or {}, text_input, context, language
)
try:
_LOGGER.info("Triggering intent handler %s", handler)
@ -218,7 +224,15 @@ class ServiceIntentHandler(IntentHandler):
class Intent:
"""Hold the intent."""
__slots__ = ["hass", "platform", "intent_type", "slots", "text_input", "context"]
__slots__ = [
"hass",
"platform",
"intent_type",
"slots",
"text_input",
"context",
"language",
]
def __init__(
self,
@ -228,6 +242,7 @@ class Intent:
slots: _SlotsType,
text_input: str | None,
context: Context,
language: str,
) -> None:
"""Initialize an intent."""
self.hass = hass
@ -236,36 +251,52 @@ class Intent:
self.slots = slots
self.text_input = text_input
self.context = context
self.language = language
@callback
def create_response(self) -> IntentResponse:
"""Create a response."""
return IntentResponse(self)
return IntentResponse(self, language=self.language)
class IntentResponse:
"""Response to an intent."""
def __init__(self, intent: Intent | None = None) -> None:
def __init__(
self, intent: Intent | None = None, language: str | None = None
) -> None:
"""Initialize an IntentResponse."""
self.intent = intent
self.speech: dict[str, dict[str, Any]] = {}
self.reprompt: dict[str, dict[str, Any]] = {}
self.card: dict[str, dict[str, str]] = {}
self.language = language
@callback
def async_set_speech(
self, speech: str, speech_type: str = "plain", extra_data: Any | None = None
self,
speech: str,
speech_type: str = "plain",
extra_data: Any | None = None,
) -> None:
"""Set speech response."""
self.speech[speech_type] = {"speech": speech, "extra_data": extra_data}
self.speech[speech_type] = {
"speech": speech,
"extra_data": extra_data,
}
@callback
def async_set_reprompt(
self, speech: str, speech_type: str = "plain", extra_data: Any | None = None
self,
speech: str,
speech_type: str = "plain",
extra_data: Any | None = None,
) -> None:
"""Set reprompt response."""
self.reprompt[speech_type] = {"reprompt": speech, "extra_data": extra_data}
self.reprompt[speech_type] = {
"reprompt": speech,
"extra_data": extra_data,
}
@callback
def async_set_card(
@ -275,10 +306,15 @@ class IntentResponse:
self.card[card_type] = {"title": title, "content": content}
@callback
def as_dict(self) -> dict[str, dict[str, dict[str, Any]]]:
def as_dict(self) -> dict[str, Any]:
"""Return a dictionary representation of an intent response."""
return (
{"speech": self.speech, "reprompt": self.reprompt, "card": self.card}
if self.reprompt
else {"speech": self.speech, "card": self.card}
)
response_dict: dict[str, Any] = {
"speech": self.speech,
"card": self.card,
"language": self.language,
}
if self.reprompt:
response_dict["reprompt"] = self.reprompt
return response_dict

View File

@ -125,7 +125,13 @@ async def test_http_processing_intent(hass, hass_client, hass_admin_user):
"card": {
"simple": {"content": "You chose a Grolsch.", "title": "Beer ordered"}
},
"speech": {"plain": {"extra_data": None, "speech": "I've ordered a Grolsch!"}},
"speech": {
"plain": {
"extra_data": None,
"speech": "I've ordered a Grolsch!",
}
},
"language": hass.config.language,
}
@ -248,10 +254,10 @@ async def test_custom_agent(hass, hass_client, hass_admin_user):
class MyAgent(conversation.AbstractConversationAgent):
"""Test Agent."""
async def async_process(self, text, context, conversation_id):
async def async_process(self, text, context, conversation_id, language):
"""Process some text."""
calls.append((text, context, conversation_id))
response = intent.IntentResponse()
calls.append((text, context, conversation_id, language))
response = intent.IntentResponse(language=language)
response.async_set_speech("Test response")
return response
@ -263,15 +269,26 @@ async def test_custom_agent(hass, hass_client, hass_admin_user):
resp = await client.post(
"/api/conversation/process",
json={"text": "Test Text", "conversation_id": "test-conv-id"},
json={
"text": "Test Text",
"conversation_id": "test-conv-id",
"language": "test-language",
},
)
assert resp.status == HTTPStatus.OK
assert await resp.json() == {
"card": {},
"speech": {"plain": {"extra_data": None, "speech": "Test response"}},
"speech": {
"plain": {
"extra_data": None,
"speech": "Test response",
}
},
"language": "test-language",
}
assert len(calls) == 1
assert calls[0][0] == "Test Text"
assert calls[0][1].user_id == hass_admin_user.id
assert calls[0][2] == "test-conv-id"
assert calls[0][3] == "test-language"

View File

@ -46,7 +46,13 @@ async def test_http_handle_intent(hass, hass_client, hass_admin_user):
"card": {
"simple": {"content": "You chose a Belgian.", "title": "Beer ordered"}
},
"speech": {"plain": {"extra_data": None, "speech": "I've ordered a Belgian!"}},
"speech": {
"plain": {
"extra_data": None,
"speech": "I've ordered a Belgian!",
}
},
"language": hass.config.language,
}