From 0de322fb95c780c81a0fc9dc579bdae626893720 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Thu, 21 Dec 2023 00:28:27 +0100 Subject: [PATCH] [assistant.google] Propagate plugin name as a string to events. This also makes it easier to programmatically stop conversations on `SpeechRecognizedEvent` with a matched phrase. --- platypush/message/event/__init__.py | 2 + platypush/message/event/assistant/__init__.py | 41 +++++++++------- platypush/plugins/assistant/__init__.py | 47 ++++++++++++------- .../plugins/assistant/google/__init__.py | 14 +++++- 4 files changed, 69 insertions(+), 35 deletions(-) diff --git a/platypush/message/event/__init__.py b/platypush/message/event/__init__.py index 1b56553a..f6bcf3be 100644 --- a/platypush/message/event/__init__.py +++ b/platypush/message/event/__init__.py @@ -255,6 +255,8 @@ class Event(Message): else: result.score = 0 + return result + def __str__(self): """ Overrides the str() operator and converts diff --git a/platypush/message/event/assistant/__init__.py b/platypush/message/event/assistant/__init__.py index f2dea592..364eace3 100644 --- a/platypush/message/event/assistant/__init__.py +++ b/platypush/message/event/assistant/__init__.py @@ -1,15 +1,27 @@ import re import sys +from typing import Optional +from platypush.context import get_plugin from platypush.message.event import Event class AssistantEvent(Event): """Base class for assistant events""" - def __init__(self, *args, assistant=None, **kwargs): - super().__init__(*args, **kwargs) - self._assistant = assistant + def __init__(self, *args, assistant: Optional[str] = None, **kwargs): + """ + :param assistant: Name of the assistant plugin that triggered the event. + """ + super().__init__(*args, assistant=assistant, **kwargs) + + @property + def _assistant(self): + return ( + get_plugin(self.args.get('assistant')) + if self.args.get('assistant') + else None + ) class ConversationStartEvent(AssistantEvent): @@ -26,10 +38,10 @@ class ConversationEndEvent(AssistantEvent): Event triggered when a conversation ends """ - def __init__(self, *args, with_follow_on_turn=False, **kwargs): + def __init__(self, *args, with_follow_on_turn: bool = False, **kwargs): """ - :param with_follow_on_turn: Set to true if the conversation expects a user follow-up, false otherwise - :type with_follow_on_turn: str + :param with_follow_on_turn: Set to true if the conversation expects a + user follow-up, false otherwise """ super().__init__(*args, with_follow_on_turn=with_follow_on_turn, **kwargs) @@ -49,10 +61,9 @@ class ResponseEvent(ConversationEndEvent): Event triggered when a response is processed by the assistant """ - def __init__(self, response_text, *args, **kwargs): + def __init__(self, *args, response_text: str, **kwargs): """ :param response_text: Response text processed by the assistant - :type response_text: str """ super().__init__(*args, response_text=response_text, **kwargs) @@ -69,10 +80,9 @@ class SpeechRecognizedEvent(AssistantEvent): Event triggered when a speech is recognized """ - def __init__(self, phrase, *args, **kwargs): + def __init__(self, *args, phrase: str, **kwargs): """ :param phrase: Recognized user phrase - :type phrase: str """ super().__init__(*args, phrase=phrase, **kwargs) @@ -81,7 +91,7 @@ class SpeechRecognizedEvent(AssistantEvent): def matches_condition(self, condition): """ Overrides matches condition, and stops the conversation to prevent the - 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) @@ -167,20 +177,19 @@ class HotwordDetectedEvent(AssistantEvent): Event triggered when a custom hotword is detected """ - def __init__(self, *args, hotword=None, **kwargs): + def __init__(self, *args, hotword: Optional[str] = None, **kwargs): """ - :param hotword: The detected user hotword - :type hotword: str + :param hotword: The detected user hotword. """ super().__init__(*args, hotword=hotword, **kwargs) class VolumeChangedEvent(AssistantEvent): """ - Event triggered when the volume of the assistant changes + Event triggered when the volume of the assistant changes. """ - def __init__(self, volume, *args, **kwargs): + def __init__(self, *args, volume: float, **kwargs): super().__init__(*args, volume=volume, **kwargs) diff --git a/platypush/plugins/assistant/__init__.py b/platypush/plugins/assistant/__init__.py index 658192db..0c88d838 100644 --- a/platypush/plugins/assistant/__init__.py +++ b/platypush/plugins/assistant/__init__.py @@ -3,7 +3,7 @@ from dataclasses import asdict, dataclass from enum import Enum import os from threading import Event -from typing import Any, Collection, Dict, Optional +from typing import Any, Collection, Dict, Optional, Type from platypush.context import get_bus, get_plugin from platypush.entities.assistants import Assistant @@ -67,7 +67,8 @@ class AssistantPlugin(Plugin, AssistantEntityManager, ABC): tts_plugin: Optional[str] = None, tts_plugin_args: Optional[Dict[str, Any]] = None, conversation_start_sound: Optional[str] = None, - **kwargs + stop_conversation_on_speech_match: bool = False, + **kwargs, ): """ :param tts_plugin: If set, the assistant will use this plugin (e.g. @@ -81,10 +82,19 @@ class AssistantPlugin(Plugin, AssistantEntityManager, ABC): audio file when it detects a speech. The sound file will be played on the default audio output device. If not set, the assistant won't play any sound when it detects a speech. + + :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__(*args, **kwargs) self.tts_plugin = tts_plugin self.tts_plugin_args = tts_plugin_args or {} + self.stop_conversation_on_speech_match = stop_conversation_on_speech_match self._conversation_start_sound = None if conversation_start_sound: self._conversation_start_sound = os.path.abspath( @@ -97,6 +107,7 @@ class AssistantPlugin(Plugin, AssistantEntityManager, ABC): self._last_query: Optional[str] = None self._last_response: Optional[str] = None self._cur_alert_type: Optional[AlertType] = None + self._plugin_name = get_plugin_name_by_class(type(self)) @property def _state(self) -> AssistantState: @@ -189,35 +200,35 @@ class AssistantPlugin(Plugin, AssistantEntityManager, ABC): audio.play(self._conversation_start_sound) - def _send_event(self, event: AssistantEvent): + def _send_event(self, event_type: Type[AssistantEvent], **kwargs): self.publish_entities([self]) - get_bus().post(event) + get_bus().post(event_type(assistant=self._plugin_name, **kwargs)) def _on_conversation_start(self): self._last_response = None self._last_query = None self._conversation_running.set() - self._send_event(ConversationStartEvent(assistant=self)) + self._send_event(ConversationStartEvent) self._play_conversation_start_sound() def _on_conversation_end(self): self._conversation_running.clear() - self._send_event(ConversationEndEvent(assistant=self)) + self._send_event(ConversationEndEvent) def _on_conversation_timeout(self): self._last_response = None self._last_query = None self._conversation_running.clear() - self._send_event(ConversationTimeoutEvent(assistant=self)) + self._send_event(ConversationTimeoutEvent) def _on_no_response(self): self._last_response = None self._conversation_running.clear() - self._send_event(NoResponseEvent(assistant=self)) + self._send_event(NoResponseEvent) def _on_reponse_rendered(self, text: Optional[str]): self._last_response = text - self._send_event(ResponseEvent(assistant=self, response_text=text)) + self._send_event(ResponseEvent, response_text=text) tts = self._get_tts_plugin() if tts and text: @@ -227,39 +238,39 @@ class AssistantPlugin(Plugin, AssistantEntityManager, ABC): def _on_speech_recognized(self, phrase: Optional[str]): phrase = (phrase or '').lower().strip() self._last_query = phrase - self._send_event(SpeechRecognizedEvent(assistant=self, phrase=phrase)) + self._send_event(SpeechRecognizedEvent, phrase=phrase) def _on_alarm_start(self): self._cur_alert_type = AlertType.ALARM - self._send_event(AlarmStartedEvent(assistant=self)) + self._send_event(AlarmStartedEvent) def _on_alarm_end(self): self._cur_alert_type = None - self._send_event(AlarmEndEvent(assistant=self)) + self._send_event(AlarmEndEvent) def _on_timer_start(self): self._cur_alert_type = AlertType.TIMER - self._send_event(TimerStartedEvent(assistant=self)) + self._send_event(TimerStartedEvent) def _on_timer_end(self): self._cur_alert_type = None - self._send_event(TimerEndEvent(assistant=self)) + self._send_event(TimerEndEvent) def _on_alert_start(self): self._cur_alert_type = AlertType.ALERT - self._send_event(AlertStartedEvent(assistant=self)) + self._send_event(AlertStartedEvent) def _on_alert_end(self): self._cur_alert_type = None - self._send_event(AlertEndEvent(assistant=self)) + self._send_event(AlertEndEvent) def _on_mute(self): self._is_muted = True - self._send_event(MicMutedEvent(assistant=self)) + self._send_event(MicMutedEvent) def _on_unmute(self): self._is_muted = False - self._send_event(MicUnmutedEvent(assistant=self)) + self._send_event(MicUnmutedEvent) def _on_mute_changed(self, value: bool): if value: diff --git a/platypush/plugins/assistant/google/__init__.py b/platypush/plugins/assistant/google/__init__.py index a5ce3b4a..0a0ce78f 100644 --- a/platypush/plugins/assistant/google/__init__.py +++ b/platypush/plugins/assistant/google/__init__.py @@ -76,6 +76,7 @@ class AssistantGooglePlugin(AssistantPlugin, RunnablePlugin): self, credentials_file: Optional[str] = None, device_model_id: str = 'Platypush', + stop_conversation_on_speech_match: bool = True, **kwargs, ): """ @@ -94,9 +95,20 @@ class AssistantGooglePlugin(AssistantPlugin, RunnablePlugin): :param device_model_id: The device model ID that identifies the device where the assistant is running (default: Platypush). It can be a 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.device_model_id = device_model_id self.credentials = None