Michael Hansen e16f17f5a8
Voice assistant integration with pipelines (#89822)
* Initial commit

* Add websocket test tool

* Small tweak

* Tiny cleanup

* Make pipeline work with frontend branch

* Add some more info to start event

* Fixes

* First voice assistant tests

* Remove run_task

* Clean up for PR

* Add config_flow.py

* Remove CLI tool

* Simplify by removing stt/tts for now

* Clean up and fix tests

* More clean up and API changes

* Add quality_scale

* Remove data from run-finish

* Use StrEnum backport

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2023-03-16 20:42:26 -04:00

125 lines
3.2 KiB
Python

"""Classes for voice assistant pipelines."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any
from homeassistant.backports.enum import StrEnum
from homeassistant.components import conversation
from homeassistant.core import Context, HomeAssistant
from homeassistant.util.dt import utcnow
DEFAULT_TIMEOUT = 30 # seconds
@dataclass
class PipelineRequest:
"""Request to start a pipeline run."""
intent_input: str
conversation_id: str | None = None
class PipelineEventType(StrEnum):
"""Event types emitted during a pipeline run."""
RUN_START = "run-start"
RUN_FINISH = "run-finish"
INTENT_START = "intent-start"
INTENT_FINISH = "intent-finish"
ERROR = "error"
@dataclass
class PipelineEvent:
"""Events emitted during a pipeline run."""
type: PipelineEventType
data: dict[str, Any] | None = None
timestamp: str = field(default_factory=lambda: utcnow().isoformat())
def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the event."""
return {
"type": self.type,
"timestamp": self.timestamp,
"data": self.data or {},
}
@dataclass
class Pipeline:
"""A voice assistant pipeline."""
name: str
language: str | None
conversation_engine: str | None
async def run(
self,
hass: HomeAssistant,
context: Context,
request: PipelineRequest,
event_callback: Callable[[PipelineEvent], None],
timeout: int | float | None = DEFAULT_TIMEOUT,
) -> None:
"""Run a pipeline with an optional timeout."""
await asyncio.wait_for(
self._run(hass, context, request, event_callback), timeout=timeout
)
async def _run(
self,
hass: HomeAssistant,
context: Context,
request: PipelineRequest,
event_callback: Callable[[PipelineEvent], None],
) -> None:
"""Run a pipeline."""
language = self.language or hass.config.language
event_callback(
PipelineEvent(
PipelineEventType.RUN_START,
{
"pipeline": self.name,
"language": language,
},
)
)
intent_input = request.intent_input
event_callback(
PipelineEvent(
PipelineEventType.INTENT_START,
{
"engine": self.conversation_engine or "default",
"intent_input": intent_input,
},
)
)
conversation_result = await conversation.async_converse(
hass=hass,
text=intent_input,
conversation_id=request.conversation_id,
context=context,
language=language,
agent_id=self.conversation_engine,
)
event_callback(
PipelineEvent(
PipelineEventType.INTENT_FINISH,
{"intent_output": conversation_result.as_dict()},
)
)
event_callback(
PipelineEvent(
PipelineEventType.RUN_FINISH,
)
)