mirror of
https://github.com/home-assistant/core.git
synced 2025-04-22 16:27:56 +00:00
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:
parent
ee8a2d1e25
commit
ac87528bec
@ -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
|
||||
|
@ -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
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user