Add conversation mobile app webhook (#86239)

* Add conversation mobile app webhook

* Re-instate removed unused import which was used as fixture
This commit is contained in:
Paulus Schoutsen 2023-01-19 13:59:02 -05:00 committed by GitHub
parent c0d9dcdb3f
commit 9631146745
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 151 additions and 38 deletions

View File

@ -33,11 +33,18 @@ SERVICE_PROCESS = "process"
SERVICE_RELOAD = "reload"
SERVICE_PROCESS_SCHEMA = vol.Schema(
{vol.Required(ATTR_TEXT): cv.string, vol.Optional(ATTR_LANGUAGE): cv.string}
{
vol.Required(ATTR_TEXT): cv.string,
vol.Optional(ATTR_LANGUAGE): cv.string,
}
)
SERVICE_RELOAD_SCHEMA = vol.Schema({vol.Optional(ATTR_LANGUAGE): cv.string})
SERVICE_RELOAD_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_LANGUAGE): cv.string,
}
)
CONFIG_SCHEMA = vol.Schema(
@ -101,8 +108,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@websocket_api.websocket_command(
{
"type": "conversation/process",
"text": str,
vol.Required("type"): "conversation/process",
vol.Required("text"): str,
vol.Optional("conversation_id"): vol.Any(str, None),
vol.Optional("language"): str,
}
@ -114,7 +121,7 @@ async def websocket_process(
msg: dict[str, Any],
) -> None:
"""Process text."""
result = await _async_converse(
result = await async_converse(
hass,
msg["text"],
msg.get("conversation_id"),
@ -142,7 +149,11 @@ async def websocket_prepare(
connection.send_result(msg["id"])
@websocket_api.websocket_command({"type": "conversation/agent/info"})
@websocket_api.websocket_command(
{
vol.Required("type"): "conversation/agent/info",
}
)
@websocket_api.async_response
async def websocket_get_agent_info(
hass: HomeAssistant,
@ -161,7 +172,12 @@ async def websocket_get_agent_info(
)
@websocket_api.websocket_command({"type": "conversation/onboarding/set", "shown": bool})
@websocket_api.websocket_command(
{
vol.Required("type"): "conversation/onboarding/set",
vol.Required("shown"): bool,
}
)
@websocket_api.async_response
async def websocket_set_onboarding(
hass: HomeAssistant,
@ -197,7 +213,7 @@ class ConversationProcessView(http.HomeAssistantView):
async def post(self, request, data):
"""Send a request for processing."""
hass = request.app["hass"]
result = await _async_converse(
result = await async_converse(
hass,
text=data["text"],
conversation_id=data.get("conversation_id"),
@ -216,7 +232,7 @@ async def _get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent:
return agent
async def _async_converse(
async def async_converse(
hass: core.HomeAssistant,
text: str,
conversation_id: str | None,

View File

@ -67,14 +67,13 @@ class DefaultAgent(AbstractConversationAgent):
if "intent" not in self.hass.config.components:
await setup.async_setup_component(self.hass, "intent", {})
config = config.get(DOMAIN, {})
self.hass.data.setdefault(DOMAIN, {})
if config:
if config and config.get(DOMAIN):
_LOGGER.warning(
"Custom intent sentences have been moved to config/custom_sentences"
)
self.hass.data.setdefault(DOMAIN, {})
async def async_process(
self,
text: str,

View File

@ -5,7 +5,7 @@
"documentation": "https://www.home-assistant.io/integrations/mobile_app",
"requirements": ["PyNaCl==1.5.0"],
"dependencies": ["http", "webhook", "person", "tag", "websocket_api"],
"after_dependencies": ["cloud", "camera", "notify"],
"after_dependencies": ["cloud", "camera", "conversation", "notify"],
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal",
"iot_class": "local_push",

View File

@ -15,7 +15,13 @@ from nacl.exceptions import CryptoError
from nacl.secret import SecretBox
import voluptuous as vol
from homeassistant.components import camera, cloud, notify as hass_notify, tag
from homeassistant.components import (
camera,
cloud,
conversation,
notify as hass_notify,
tag,
)
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES as BINARY_SENSOR_CLASSES,
)
@ -301,6 +307,28 @@ async def webhook_fire_event(
return empty_okay_response()
@WEBHOOK_COMMANDS.register("conversation_process")
@validate_schema(
{
vol.Required("text"): cv.string,
vol.Optional("language"): cv.string,
vol.Optional("conversation_id"): cv.string,
}
)
async def webhook_conversation_process(
hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any]
) -> Response:
"""Handle a conversation process webhook."""
result = await conversation.async_converse(
hass,
text=data["text"],
language=data.get("language"),
conversation_id=data.get("conversation_id"),
context=registration_context(config_entry.data),
)
return webhook_response(result.as_dict(), registration=config_entry.data)
@WEBHOOK_COMMANDS.register("stream_camera")
@validate_schema({vol.Required(ATTR_CAMERA_ENTITY_ID): cv.string})
async def webhook_stream_camera(

View File

@ -1 +1,30 @@
"""Tests for the conversation component."""
from __future__ import annotations
from homeassistant.components import conversation
from homeassistant.core import Context
from homeassistant.helpers import intent
class MockAgent(conversation.AbstractConversationAgent):
"""Test Agent."""
def __init__(self) -> None:
"""Initialize the agent."""
self.calls = []
self.response = "Test response"
async def async_process(
self,
text: str,
context: Context,
conversation_id: str | None = None,
language: str | None = None,
) -> conversation.ConversationResult | None:
"""Process some text."""
self.calls.append((text, context, conversation_id, language))
response = intent.IntentResponse(language=language)
response.async_set_speech(self.response)
return conversation.ConversationResult(
response=response, conversation_id=conversation_id
)

View File

@ -0,0 +1,15 @@
"""Conversation test helpers."""
import pytest
from homeassistant.components import conversation
from . import MockAgent
@pytest.fixture
def mock_agent(hass):
"""Mock agent."""
agent = MockAgent()
conversation.async_set_agent(hass, agent)
return agent

View File

@ -222,25 +222,8 @@ async def test_http_api_wrong_data(hass, init_components, hass_client):
assert resp.status == HTTPStatus.BAD_REQUEST
async def test_custom_agent(hass, hass_client, hass_admin_user):
async def test_custom_agent(hass, hass_client, hass_admin_user, mock_agent):
"""Test a custom conversation agent."""
calls = []
class MyAgent(conversation.AbstractConversationAgent):
"""Test Agent."""
async def async_process(self, text, context, conversation_id, language):
"""Process some text."""
calls.append((text, context, conversation_id, language))
response = intent.IntentResponse(language=language)
response.async_set_speech("Test response")
return conversation.ConversationResult(
response=response, conversation_id=conversation_id
)
conversation.async_set_agent(hass, MyAgent())
assert await async_setup_component(hass, "conversation", {})
client = await hass_client()
@ -270,11 +253,11 @@ async def test_custom_agent(hass, hass_client, hass_admin_user):
"conversation_id": "test-conv-id",
}
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"
assert len(mock_agent.calls) == 1
assert mock_agent.calls[0][0] == "Test Text"
assert mock_agent.calls[0][1].user_id == hass_admin_user.id
assert mock_agent.calls[0][2] == "test-conv-id"
assert mock_agent.calls[0][3] == "test-language"
@pytest.mark.parametrize(

View File

@ -22,6 +22,10 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE
from tests.common import async_capture_events, async_mock_service
from tests.components.conversation.conftest import mock_agent
# To avoid autoflake8 removing the import
mock_agent = mock_agent
def encrypt_payload(secret_key, payload, encode_json=True):
@ -974,3 +978,42 @@ async def test_reregister_sensor(hass, create_registrations, webhook_client):
assert reg_resp.status == HTTPStatus.CREATED
entry = ent_reg.async_get("sensor.test_1_battery_state")
assert entry.disabled_by is None
async def test_webhook_handle_conversation_process(
hass, create_registrations, webhook_client, mock_agent
):
"""Test that we can converse."""
webhook_client.server.app.router._frozen = False
resp = await webhook_client.post(
"/api/webhook/{}".format(create_registrations[1]["webhook_id"]),
json={
"type": "conversation_process",
"data": {
"text": "Turn the kitchen light off",
},
},
)
assert resp.status == HTTPStatus.OK
json = await resp.json()
assert json == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Test response",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [],
"failed": [],
},
},
"conversation_id": None,
}