mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +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
|
return True
|
||||||
|
|
||||||
async def async_process(
|
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:
|
) -> intent.IntentResponse:
|
||||||
"""Process a sentence."""
|
"""Process a sentence."""
|
||||||
response = await self.api.async_converse_text(text, conversation_id)
|
response = await self.api.async_converse_text(text, conversation_id)
|
||||||
@ -310,6 +314,6 @@ class AlmondAgent(conversation.AbstractConversationAgent):
|
|||||||
buffer += ","
|
buffer += ","
|
||||||
buffer += f" {message['title']}"
|
buffer += f" {message['title']}"
|
||||||
|
|
||||||
intent_result = intent.IntentResponse()
|
intent_result = intent.IntentResponse(language=language)
|
||||||
intent_result.async_set_speech(buffer.strip())
|
intent_result.async_set_speech(buffer.strip())
|
||||||
return intent_result
|
return intent_result
|
||||||
|
@ -22,6 +22,7 @@ from .default_agent import DefaultAgent, async_register
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_TEXT = "text"
|
ATTR_TEXT = "text"
|
||||||
|
ATTR_LANGUAGE = "language"
|
||||||
|
|
||||||
DOMAIN = "conversation"
|
DOMAIN = "conversation"
|
||||||
|
|
||||||
@ -31,7 +32,9 @@ DATA_CONFIG = "conversation_config"
|
|||||||
|
|
||||||
SERVICE_PROCESS = "process"
|
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(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
@ -66,7 +69,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
_LOGGER.debug("Processing: <%s>", text)
|
_LOGGER.debug("Processing: <%s>", text)
|
||||||
agent = await _get_agent(hass)
|
agent = await _get_agent(hass)
|
||||||
try:
|
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:
|
except intent.IntentHandleError as err:
|
||||||
_LOGGER.error("Error processing %s: %s", text, 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(
|
@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
|
@websocket_api.async_response
|
||||||
async def websocket_process(
|
async def websocket_process(
|
||||||
@ -94,7 +104,11 @@ async def websocket_process(
|
|||||||
connection.send_result(
|
connection.send_result(
|
||||||
msg["id"],
|
msg["id"],
|
||||||
await _async_converse(
|
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"
|
name = "api:conversation:process"
|
||||||
|
|
||||||
@RequestDataValidator(
|
@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):
|
async def post(self, request, data):
|
||||||
"""Send a request for processing."""
|
"""Send a request for processing."""
|
||||||
@ -151,7 +171,11 @@ class ConversationProcessView(http.HomeAssistantView):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
intent_result = await _async_converse(
|
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:
|
except intent.IntentError as err:
|
||||||
_LOGGER.error("Error handling intent: %s", err)
|
_LOGGER.error("Error handling intent: %s", err)
|
||||||
@ -182,17 +206,20 @@ async def _async_converse(
|
|||||||
text: str,
|
text: str,
|
||||||
conversation_id: str | None,
|
conversation_id: str | None,
|
||||||
context: core.Context,
|
context: core.Context,
|
||||||
|
language: str | None = None,
|
||||||
) -> intent.IntentResponse:
|
) -> intent.IntentResponse:
|
||||||
"""Process text and get intent."""
|
"""Process text and get intent."""
|
||||||
agent = await _get_agent(hass)
|
agent = await _get_agent(hass)
|
||||||
try:
|
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:
|
except intent.IntentHandleError as err:
|
||||||
intent_result = intent.IntentResponse()
|
intent_result = intent.IntentResponse(language=language)
|
||||||
intent_result.async_set_speech(str(err))
|
intent_result.async_set_speech(str(err))
|
||||||
|
|
||||||
if intent_result is None:
|
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")
|
intent_result.async_set_speech("Sorry, I didn't understand that")
|
||||||
|
|
||||||
return intent_result
|
return intent_result
|
||||||
|
@ -25,6 +25,10 @@ class AbstractConversationAgent(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def async_process(
|
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:
|
) -> intent.IntentResponse | None:
|
||||||
"""Process a sentence."""
|
"""Process a sentence."""
|
||||||
|
@ -111,7 +111,11 @@ class DefaultAgent(AbstractConversationAgent):
|
|||||||
async_register(self.hass, intent_type, sentences)
|
async_register(self.hass, intent_type, sentences)
|
||||||
|
|
||||||
async def async_process(
|
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:
|
) -> intent.IntentResponse | None:
|
||||||
"""Process a sentence."""
|
"""Process a sentence."""
|
||||||
intents = self.hass.data[DOMAIN]
|
intents = self.hass.data[DOMAIN]
|
||||||
@ -128,6 +132,7 @@ class DefaultAgent(AbstractConversationAgent):
|
|||||||
{key: {"value": value} for key, value in match.groupdict().items()},
|
{key: {"value": value} for key, value in match.groupdict().items()},
|
||||||
text,
|
text,
|
||||||
context,
|
context,
|
||||||
|
language,
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
@ -56,6 +56,7 @@ async def async_handle(
|
|||||||
slots: _SlotsType | None = None,
|
slots: _SlotsType | None = None,
|
||||||
text_input: str | None = None,
|
text_input: str | None = None,
|
||||||
context: Context | None = None,
|
context: Context | None = None,
|
||||||
|
language: str | None = None,
|
||||||
) -> IntentResponse:
|
) -> IntentResponse:
|
||||||
"""Handle an intent."""
|
"""Handle an intent."""
|
||||||
handler: IntentHandler = hass.data.get(DATA_KEY, {}).get(intent_type)
|
handler: IntentHandler = hass.data.get(DATA_KEY, {}).get(intent_type)
|
||||||
@ -66,7 +67,12 @@ async def async_handle(
|
|||||||
if context is None:
|
if context is None:
|
||||||
context = Context()
|
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:
|
try:
|
||||||
_LOGGER.info("Triggering intent handler %s", handler)
|
_LOGGER.info("Triggering intent handler %s", handler)
|
||||||
@ -218,7 +224,15 @@ class ServiceIntentHandler(IntentHandler):
|
|||||||
class Intent:
|
class Intent:
|
||||||
"""Hold the 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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -228,6 +242,7 @@ class Intent:
|
|||||||
slots: _SlotsType,
|
slots: _SlotsType,
|
||||||
text_input: str | None,
|
text_input: str | None,
|
||||||
context: Context,
|
context: Context,
|
||||||
|
language: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an intent."""
|
"""Initialize an intent."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
@ -236,36 +251,52 @@ class Intent:
|
|||||||
self.slots = slots
|
self.slots = slots
|
||||||
self.text_input = text_input
|
self.text_input = text_input
|
||||||
self.context = context
|
self.context = context
|
||||||
|
self.language = language
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def create_response(self) -> IntentResponse:
|
def create_response(self) -> IntentResponse:
|
||||||
"""Create a response."""
|
"""Create a response."""
|
||||||
return IntentResponse(self)
|
return IntentResponse(self, language=self.language)
|
||||||
|
|
||||||
|
|
||||||
class IntentResponse:
|
class IntentResponse:
|
||||||
"""Response to an intent."""
|
"""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."""
|
"""Initialize an IntentResponse."""
|
||||||
self.intent = intent
|
self.intent = intent
|
||||||
self.speech: dict[str, dict[str, Any]] = {}
|
self.speech: dict[str, dict[str, Any]] = {}
|
||||||
self.reprompt: dict[str, dict[str, Any]] = {}
|
self.reprompt: dict[str, dict[str, Any]] = {}
|
||||||
self.card: dict[str, dict[str, str]] = {}
|
self.card: dict[str, dict[str, str]] = {}
|
||||||
|
self.language = language
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_set_speech(
|
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:
|
) -> None:
|
||||||
"""Set speech response."""
|
"""Set speech response."""
|
||||||
self.speech[speech_type] = {"speech": speech, "extra_data": extra_data}
|
self.speech[speech_type] = {
|
||||||
|
"speech": speech,
|
||||||
|
"extra_data": extra_data,
|
||||||
|
}
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_set_reprompt(
|
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:
|
) -> None:
|
||||||
"""Set reprompt response."""
|
"""Set reprompt response."""
|
||||||
self.reprompt[speech_type] = {"reprompt": speech, "extra_data": extra_data}
|
self.reprompt[speech_type] = {
|
||||||
|
"reprompt": speech,
|
||||||
|
"extra_data": extra_data,
|
||||||
|
}
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_set_card(
|
def async_set_card(
|
||||||
@ -275,10 +306,15 @@ class IntentResponse:
|
|||||||
self.card[card_type] = {"title": title, "content": content}
|
self.card[card_type] = {"title": title, "content": content}
|
||||||
|
|
||||||
@callback
|
@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 a dictionary representation of an intent response."""
|
||||||
return (
|
response_dict: dict[str, Any] = {
|
||||||
{"speech": self.speech, "reprompt": self.reprompt, "card": self.card}
|
"speech": self.speech,
|
||||||
if self.reprompt
|
"card": self.card,
|
||||||
else {"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": {
|
"card": {
|
||||||
"simple": {"content": "You chose a Grolsch.", "title": "Beer ordered"}
|
"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):
|
class MyAgent(conversation.AbstractConversationAgent):
|
||||||
"""Test Agent."""
|
"""Test Agent."""
|
||||||
|
|
||||||
async def async_process(self, text, context, conversation_id):
|
async def async_process(self, text, context, conversation_id, language):
|
||||||
"""Process some text."""
|
"""Process some text."""
|
||||||
calls.append((text, context, conversation_id))
|
calls.append((text, context, conversation_id, language))
|
||||||
response = intent.IntentResponse()
|
response = intent.IntentResponse(language=language)
|
||||||
response.async_set_speech("Test response")
|
response.async_set_speech("Test response")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@ -263,15 +269,26 @@ async def test_custom_agent(hass, hass_client, hass_admin_user):
|
|||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/api/conversation/process",
|
"/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 resp.status == HTTPStatus.OK
|
||||||
assert await resp.json() == {
|
assert await resp.json() == {
|
||||||
"card": {},
|
"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 len(calls) == 1
|
||||||
assert calls[0][0] == "Test Text"
|
assert calls[0][0] == "Test Text"
|
||||||
assert calls[0][1].user_id == hass_admin_user.id
|
assert calls[0][1].user_id == hass_admin_user.id
|
||||||
assert calls[0][2] == "test-conv-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": {
|
"card": {
|
||||||
"simple": {"content": "You chose a Belgian.", "title": "Beer ordered"}
|
"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