1 """HTTP endpoints for conversation integration."""
3 from __future__
import annotations
5 from collections.abc
import Iterable
8 from aiohttp
import web
9 from hassil.recognize
import MISSING_ENTITY, RecognizeResult
10 from hassil.string_matcher
import UnmatchedRangeEntity, UnmatchedTextEntity
11 import voluptuous
as vol
20 from .agent_manager
import (
26 from .const
import DATA_COMPONENT, DATA_DEFAULT_ENTITY
27 from .default_agent
import METADATA_CUSTOM_FILE, METADATA_CUSTOM_SENTENCE, DefaultAgent
28 from .entity
import ConversationEntity
29 from .models
import ConversationInput
34 """Set up the HTTP API for the conversation integration."""
36 websocket_api.async_register_command(hass, websocket_process)
37 websocket_api.async_register_command(hass, websocket_prepare)
38 websocket_api.async_register_command(hass, websocket_list_agents)
39 websocket_api.async_register_command(hass, websocket_hass_agent_debug)
42 @websocket_api.websocket_command(
{
vol.Required("type"):
"conversation/process",
43 vol.Required(
"text"): str,
44 vol.Optional(
"conversation_id"): vol.Any(str,
None),
45 vol.Optional(
"language"): str,
46 vol.Optional(
"agent_id"): agent_id_validator,
49 @websocket_api.async_response
59 conversation_id=msg.get(
"conversation_id"),
60 context=connection.context(msg),
61 language=msg.get(
"language"),
62 agent_id=msg.get(
"agent_id"),
64 connection.send_result(msg[
"id"], result.as_dict())
67 @websocket_api.websocket_command(
{
"type": "conversation/prepare",
vol.Optional("language"): str,
68 vol.Optional(
"agent_id"): agent_id_validator,
71 @websocket_api.async_response
81 connection.send_error(msg[
"id"], websocket_api.ERR_NOT_FOUND,
"Agent not found")
84 await agent.async_prepare(msg.get(
"language"))
85 connection.send_result(msg[
"id"])
88 @websocket_api.websocket_command(
{
vol.Required("type"):
"conversation/agent/list",
89 vol.Optional(
"language"): str,
90 vol.Optional(
"country"): str,
93 @websocket_api.async_response
97 """List conversation agents and, optionally, if they support a given language."""
98 country = msg.get(
"country")
99 language = msg.get(
"language")
102 for entity
in hass.data[DATA_COMPONENT].entities:
103 supported_languages = entity.supported_languages
104 if language
and supported_languages != MATCH_ALL:
105 supported_languages = language_util.matches(
106 language, supported_languages, country
109 name = entity.entity_id
110 if state := hass.states.get(entity.entity_id):
115 "id": entity.entity_id,
117 "supported_languages": supported_languages,
123 for agent_info
in manager.async_get_agent_info():
124 agent = manager.async_get_agent(agent_info.id)
125 assert agent
is not None
127 if isinstance(agent, ConversationEntity):
130 supported_languages = agent.supported_languages
131 if language
and supported_languages != MATCH_ALL:
132 supported_languages = language_util.matches(
133 language, supported_languages, country
136 agent_dict: dict[str, Any] = {
138 "name": agent_info.name,
139 "supported_languages": supported_languages,
141 agents.append(agent_dict)
143 connection.send_message(websocket_api.result_message(msg[
"id"], {
"agents": agents}))
146 @websocket_api.websocket_command(
{
vol.Required("type"):
"conversation/agent/homeassistant/debug",
147 vol.Required(
"sentences"): [str],
148 vol.Optional(
"language"): str,
149 vol.Optional(
"device_id"): vol.Any(str,
None),
152 @websocket_api.async_response
156 """Return intents that would be matched by the default agent for a list of sentences."""
157 agent = hass.data.get(DATA_DEFAULT_ENTITY)
158 assert isinstance(agent, DefaultAgent)
161 result_dicts: list[dict[str, Any] |
None] = []
162 for sentence
in msg[
"sentences"]:
165 context=connection.context(msg),
166 conversation_id=
None,
167 device_id=msg.get(
"device_id"),
168 language=msg.get(
"language", hass.config.language),
171 result_dict: dict[str, Any] |
None =
None
173 if trigger_result := await agent.async_recognize_sentence_trigger(user_input):
180 "sentence_template": trigger_result.sentence_template
or "",
182 elif intent_result := await agent.async_recognize_intent(user_input):
183 successful_match =
not intent_result.unmatched_entities
187 "name": intent_result.intent.name,
191 entity_key: entity.text
or entity.value
192 for entity_key, entity
in intent_result.entities.items()
198 "value": entity.value,
201 for entity_key, entity
in intent_result.entities.items()
206 "match": successful_match,
208 "sentence_template":
"",
214 result_dict[
"targets"] = {
215 state.entity_id: {
"matched": is_matched}
219 if intent_result.intent_sentence
is not None:
220 result_dict[
"sentence_template"] = intent_result.intent_sentence.text
223 if intent_result.intent_metadata
and intent_result.intent_metadata.get(
224 METADATA_CUSTOM_SENTENCE
226 result_dict[
"source"] =
"custom"
227 result_dict[
"file"] = intent_result.intent_metadata.get(
231 result_dict[
"source"] =
"builtin"
233 result_dicts.append(result_dict)
235 connection.send_result(msg[
"id"], {
"results": result_dicts})
240 result: RecognizeResult,
241 ) -> Iterable[tuple[State, bool]]:
242 """Yield state/is_matched pairs for a hassil recognition."""
243 entities = result.entities
245 name: str |
None =
None
246 area_name: str |
None =
None
247 domains: set[str] |
None =
None
248 device_classes: set[str] |
None =
None
249 state_names: set[str] |
None =
None
251 if "name" in entities:
252 name =
str(entities[
"name"].value)
254 if "area" in entities:
255 area_name =
str(entities[
"area"].value)
257 if "domain" in entities:
258 domains = set(cv.ensure_list(entities[
"domain"].value))
260 if "device_class" in entities:
261 device_classes = set(cv.ensure_list(entities[
"device_class"].value))
263 if "state" in entities:
265 state_names = set(cv.ensure_list(entities[
"state"].value))
269 and (area_name
is None)
271 and (
not device_classes)
272 and (
not state_names)
277 states = intent.async_match_states(
282 device_classes=device_classes,
287 is_matched = (state_names
is None)
or (state.state
in state_names)
288 yield state, is_matched
292 result: RecognizeResult,
293 ) -> dict[str, str | int | float]:
294 """Return a dict of unmatched text/range slot entities."""
295 unmatched_slots: dict[str, str | int | float] = {}
296 for entity
in result.unmatched_entities_list:
297 if isinstance(entity, UnmatchedTextEntity):
298 if entity.text == MISSING_ENTITY:
303 unmatched_slots[entity.name] = entity.text
304 elif isinstance(entity, UnmatchedRangeEntity):
305 unmatched_slots[entity.name] = entity.value
307 return unmatched_slots
311 """View to process text."""
313 url =
"/api/conversation/process"
314 name =
"api:conversation:process"
316 @RequestDataValidator(
vol.Schema(
{
vol.Required("text"): str,
317 vol.Optional(
"conversation_id"): str,
318 vol.Optional(
"language"): str,
319 vol.Optional(
"agent_id"): agent_id_validator,
323 async
def post(self, request: web.Request, data: dict[str, str]) -> web.Response:
324 """Send a request for processing."""
325 hass = request.app[http.KEY_HASS]
330 conversation_id=data.get(
"conversation_id"),
331 context=self.context(request),
332 language=data.get(
"language"),
333 agent_id=data.get(
"agent_id"),
336 return self.json(result.as_dict())
337
web.Response post(self, web.Request request, dict[str, str] data)
AgentManager get_agent_manager(HomeAssistant hass)
ConversationResult async_converse(HomeAssistant hass, str text, str|None conversation_id, Context context, str|None language=None, str|None agent_id=None, str|None device_id=None)
AbstractConversationAgent|ConversationEntity|None async_get_agent(HomeAssistant hass, str|None agent_id=None)
Iterable[tuple[State, bool]] _get_debug_targets(HomeAssistant hass, RecognizeResult result)
None async_setup(HomeAssistant hass)
None websocket_list_agents(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg)
None websocket_process(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
None websocket_prepare(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
dict[str, str|int|float] _get_unmatched_slots(RecognizeResult result)
None websocket_hass_agent_debug(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg)