Several improvements for assistant plugins.

- `stop_conversation_on_speech_match` should default to True.

- `render_response` should also handle conversation follow-ups, set the
  follow-up to True if the response ends with a question mark and the
  value of `with_follow_on_turn` is not set,

- Don't render responses if a `tts_plugin` is not set.
This commit is contained in:
Fabio Manganiello 2024-06-01 12:06:35 +02:00
parent c7d640a1d2
commit fcae7aa3ad
3 changed files with 78 additions and 36 deletions

View file

@ -147,7 +147,12 @@ class SpeechRecognizedEvent(AssistantEvent):
""" """
result = super().matches_condition(condition) result = super().matches_condition(condition)
if result.is_match and self.assistant and condition.args.get('phrase'): if (
result.is_match
and condition.args.get('phrase')
and self.assistant
and self.assistant.stop_conversation_on_speech_match
):
self.assistant.stop_conversation() self.assistant.stop_conversation()
return result return result
@ -248,7 +253,11 @@ class IntentRecognizedEvent(AssistantEvent):
default assistant response if the event matched some event hook condition. default assistant response if the event matched some event hook condition.
""" """
result = super().matches_condition(condition) result = super().matches_condition(condition)
if result.is_match and self.assistant: if (
result.is_match
and self.assistant
and self.assistant.stop_conversation_on_speech_match
):
self.assistant.stop_conversation() self.assistant.stop_conversation()
return result return result

View file

@ -50,7 +50,7 @@ class AssistantPlugin(Plugin, AssistantEntityManager, ABC):
tts_plugin: Optional[str] = None, tts_plugin: Optional[str] = None,
tts_plugin_args: Optional[Dict[str, Any]] = None, tts_plugin_args: Optional[Dict[str, Any]] = None,
conversation_start_sound: Optional[str] = None, conversation_start_sound: Optional[str] = None,
stop_conversation_on_speech_match: bool = False, stop_conversation_on_speech_match: bool = True,
**kwargs, **kwargs,
): ):
""" """
@ -66,13 +66,21 @@ class AssistantPlugin(Plugin, AssistantEntityManager, ABC):
on the default audio output device. If not set, the assistant won't on the default audio output device. If not set, the assistant won't
play any sound when it detects a speech. play any sound when it detects a speech.
:param stop_conversation_on_speech_match: If set, the plugin will close the :param stop_conversation_on_speech_match: If set, the plugin will
conversation if the latest recognized speech matches a registered prevent the default assistant response when a
:class:`platypush.message.event.assistant.SpeechRecognizedEvent` hook :class:`platypush.message.event.assistant.SpeechRecognizedEvent`
with a phrase. This is usually set to ``True`` for matches a user hook with a condition on a ``phrase`` field. This is
:class:`platypush.plugins.assistant.google.GoogleAssistantPlugin`, useful to prevent the assistant from responding with a default "*I'm
as it overrides the default assistant response when a speech event is sorry, I can't help you with that*" when e.g. you say "*play the
actually handled on the application side. music*", and you have a hook that matches the phrase "*play the
music*" and handles it with a custom action. If set, and you wish
the assistant to also provide an answer if an event matches one of
your hooks, then you should call the :meth:`render_response` method
in your hook handler. If not set, then the assistant will always try
and respond with a default message, even if a speech event matches
the phrase of one of your hooks. In this case, if you want to prevent
the default response, you should call :meth:`stop_conversation`
explicitly from your hook handler. Default: True.
""" """
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.tts_plugin = tts_plugin self.tts_plugin = tts_plugin
@ -165,15 +173,38 @@ class AssistantPlugin(Plugin, AssistantEntityManager, ABC):
return asdict(self._state) return asdict(self._state)
@action @action
def render_response(self, text: str, *_, **__): def render_response(
self, text: str, *_, with_follow_on_turn: Optional[bool] = None, **__
) -> bool:
""" """
Render a response text as audio over the configured TTS plugin. Render a response text as audio over the configured TTS plugin.
:param text: Text to render. :param text: Text to render.
:param with_follow_on_turn: If set, the assistant will wait for a follow-up.
By default, ``with_follow_on_turn`` will be automatically set to true if
the ``text`` ends with a question mark.
:return: True if the assistant is waiting for a follow-up, False otherwise.
""" """
self._on_response_render_start(text) if not text:
self._on_no_response()
return False
follow_up = (
bool(text and text.strip().endswith('?'))
if with_follow_on_turn is None
else with_follow_on_turn
)
self._on_response_render_start(text, with_follow_on_turn=follow_up)
self._render_response(text) self._render_response(text)
self._on_response_render_end() self._on_response_render_end(with_follow_on_turn=follow_up)
if follow_up:
self.start_conversation()
else:
self.stop_conversation()
return follow_up
def _get_tts_plugin(self): def _get_tts_plugin(self):
if not self.tts_plugin: if not self.tts_plugin:
@ -228,22 +259,37 @@ class AssistantPlugin(Plugin, AssistantEntityManager, ABC):
self._conversation_running.clear() self._conversation_running.clear()
self._send_event(NoResponseEvent) self._send_event(NoResponseEvent)
def _on_response_render_start(self, text: Optional[str]): def _on_response_render_start(
self, text: Optional[str], with_follow_on_turn: bool = False
):
from platypush.message.event.assistant import ResponseEvent from platypush.message.event.assistant import ResponseEvent
self._last_response = text self._last_response = text
self._send_event(ResponseEvent, response_text=text) self._send_event(
ResponseEvent, response_text=text, with_follow_on_turn=with_follow_on_turn
)
def _render_response(self, text: Optional[str]): def _render_response(self, text: Optional[str]):
if not text:
return
tts = self._get_tts_plugin() tts = self._get_tts_plugin()
if tts and text: if not tts:
self.stop_conversation() self.logger.warning(
'Got a response to render, but no TTS plugin is configured: %s', text
)
return
tts.say(text=text, **self.tts_plugin_args) tts.say(text=text, **self.tts_plugin_args)
def _on_response_render_end(self): def _on_response_render_end(self, with_follow_on_turn: bool = False):
from platypush.message.event.assistant import ResponseEndEvent from platypush.message.event.assistant import ResponseEndEvent
self._send_event(ResponseEndEvent, response_text=self._last_response) self._send_event(
ResponseEndEvent,
response_text=self._last_response,
with_follow_on_turn=with_follow_on_turn,
)
def _on_hotword_detected(self, hotword: Optional[str]): def _on_hotword_detected(self, hotword: Optional[str]):
from platypush.message.event.assistant import HotwordDetectedEvent from platypush.message.event.assistant import HotwordDetectedEvent

View file

@ -76,7 +76,6 @@ class AssistantGooglePlugin(AssistantPlugin, RunnablePlugin):
self, self,
credentials_file: Optional[str] = None, credentials_file: Optional[str] = None,
device_model_id: str = 'Platypush', device_model_id: str = 'Platypush',
stop_conversation_on_speech_match: bool = True,
**kwargs, **kwargs,
): ):
""" """
@ -95,20 +94,8 @@ class AssistantGooglePlugin(AssistantPlugin, RunnablePlugin):
:param device_model_id: The device model ID that identifies the device :param device_model_id: The device model ID that identifies the device
where the assistant is running (default: Platypush). It can be a where the assistant is running (default: Platypush). It can be a
custom string. custom string.
:param stop_conversation_on_speech_match: If set, the plugin will close the
conversation if the latest recognized speech matches a registered
:class:`platypush.message.event.assistant.SpeechRecognizedEvent` hook
with a phrase. This is usually set to ``True`` for
:class:`platypush.plugins.assistant.google.GoogleAssistantPlugin`,
as it overrides the default assistant response when a speech event is
actually handled on the application side.
""" """
super().__init__(**kwargs)
super().__init__(
stop_conversation_on_speech_match=stop_conversation_on_speech_match,
**kwargs,
)
self._credentials_file = credentials_file self._credentials_file = credentials_file
self.device_model_id = device_model_id self.device_model_id = device_model_id
self.credentials = None self.credentials = None
@ -155,7 +142,7 @@ class AssistantGooglePlugin(AssistantPlugin, RunnablePlugin):
hasattr(EventType, 'ON_RENDER_RESPONSE') hasattr(EventType, 'ON_RENDER_RESPONSE')
and event.type == EventType.ON_RENDER_RESPONSE and event.type == EventType.ON_RENDER_RESPONSE
): ):
self._on_reponse_rendered(event.args.get('text')) self._on_response_render_start(event.args.get('text'))
elif ( elif (
hasattr(EventType, 'ON_RESPONDING_STARTED') hasattr(EventType, 'ON_RESPONDING_STARTED')
and event.type == EventType.ON_RESPONDING_STARTED and event.type == EventType.ON_RESPONDING_STARTED