Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/converse_intents #12

Merged
merged 2 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion ovos_workshop/decorators/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from functools import wraps

from ovos_utils.log import log_deprecation

from ovos_workshop.decorators.killable import killable_intent, killable_event
from ovos_workshop.decorators.layers import enables_layer, \
disables_layer, layer_intent, removes_layer, resets_layers, replaces_layer
from ovos_workshop.decorators.ocp import ocp_play, ocp_pause, ocp_resume, \
ocp_search, ocp_previous, ocp_featured_media
from functools import wraps


# TODO: Deprecate unused import retained for backwards-compat.
from ovos_utils import classproperty
Expand All @@ -25,7 +28,9 @@ def func_wrapper(*args, **kwargs):
ret = func(*args, **kwargs)
args[0].set_context(context, words)
return ret

return func_wrapper

return context_add_decorator


Expand All @@ -43,7 +48,9 @@ def func_wrapper(*args, **kwargs):
ret = func(*args, **kwargs)
args[0].remove_context(context)
return ret

return func_wrapper

return context_removes_decorator


Expand All @@ -68,13 +75,15 @@ def intent_file_handler(intent_file: str):
"""
Deprecated decorator for adding a method as an intent file handler.
"""

def real_decorator(func):
# Store the intent_file inside the function
# This will be used later to call register_intent_file
if not hasattr(func, 'intent_files'):
func.intent_files = []
func.intent_files.append(intent_file)
return func

log_deprecation(f"Use `@intent_handler({intent_file})`", "0.1.0")
return real_decorator

Expand Down Expand Up @@ -118,13 +127,30 @@ def converse_handler(func):
return func


def conversational_intent(intent_file):
"""Decorator for adding a method as an converse intent handler.
NOTE: only padatious intents supported, not adapt
"""

def real_decorator(func):
# Store the intent_file inside the function
# This will be used later to train intents
if not hasattr(func, 'converse_intents'):
func.converse_intents = []
func.converse_intents.append(intent_file)
return func

return real_decorator


def fallback_handler(priority: int = 50):
"""
Decorator for adding a fallback intent handler.

@param priority: Fallback priority (0-100) with lower values having higher
priority
"""

def real_decorator(func):
if not hasattr(func, 'fallback_priority'):
func.fallback_priority = priority
Expand Down
66 changes: 63 additions & 3 deletions ovos_workshop/skills/ovos.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
from json_database import JsonStorage
from lingua_franca.format import pronounce_number, join_list
from lingua_franca.parse import yes_or_no, extract_number
from ovos_config.config import Configuration
from ovos_config.locations import get_xdg_config_save_path

from ovos_backend_client.api import EmailApi, MetricsApi
from ovos_bus_client import MessageBusClient
from ovos_bus_client.apis.enclosure import EnclosureAPI
Expand All @@ -28,6 +25,8 @@
from ovos_bus_client.message import Message, dig_for_message
from ovos_bus_client.session import SessionManager, Session
from ovos_bus_client.util import get_message_lang
from ovos_config.config import Configuration
from ovos_config.locations import get_xdg_config_save_path
from ovos_plugin_manager.language import OVOSLangTranslationFactory, OVOSLangDetectionFactory
from ovos_utils import camel_case_split, classproperty
from ovos_utils.dialog import get_dialog, MustacheDialogRenderer
Expand All @@ -41,6 +40,8 @@
from ovos_utils.process_utils import RuntimeRequirements
from ovos_utils.skills import get_non_properties
from ovos_utils.sound import play_audio
from padacioso import IntentContainer

from ovos_workshop.decorators.compat import backwards_compat
from ovos_workshop.decorators.killable import AbortEvent, killable_event, \
AbortQuestion
Expand Down Expand Up @@ -186,6 +187,7 @@ def __init__(self, name: Optional[str] = None,
self.intent_service = IntentServiceInterface()
self.audio_service = None
self.intent_layers = IntentLayers()
self.converse_matchers = {}

# Skill Public API
self.public_api: Dict[str, dict] = {}
Expand Down Expand Up @@ -233,6 +235,53 @@ def handle_deactivate(self, message: Message):
@param message: `{self.skill_id}.deactivate` Message
"""

def register_converse_intent(self, intent_file, handler):
""" converse padacioso intents """
name = f'{self.skill_id}.converse:{intent_file}'
fuzzy = not self.settings.get("strict_intents", False)

for lang in self.native_langs:
self.converse_matchers[lang] = IntentContainer(fuzz=fuzzy)

resources = self.load_lang(self.res_dir, lang)
resource_file = ResourceFile(resources.types.intent, intent_file)
if resource_file.file_path is None:
self.log.error(f'Unable to find "{intent_file}"')
continue
filename = str(resource_file.file_path)

with open(filename) as f:
samples = [l.strip() for l in f.read().split("\n")
if l and not l.startswith("#")]

self.converse_matchers[lang].add_intent(name, samples)

self.add_event(name, handler, 'mycroft.skill.handler')

def _handle_converse_intents(self, message):
""" called before converse method
this gives active skills a chance to parse their own intents and
consume the utterance, see conversational_intent decorator for usage
"""
if self.lang not in self.converse_matchers:
return False

best_score = 0
response = None

for utt in message.data['utterances']:
match = self.converse_matchers[self.lang].calc_intent(utt)
if match and match["conf"] > best_score:
best_score = match["conf"]
response = message.forward(match["name"], match["entities"])

if not response or best_score < self.settings.get("min_intent_conf", 0.5):
return False

# send intent event
self.bus.emit(response)
return True

def converse(self, message: Optional[Message] = None) -> bool:
"""
Override to handle an utterance before intent parsing while this skill
Expand Down Expand Up @@ -862,6 +911,10 @@ def _register_decorated(self):
if hasattr(method, 'converse'):
self.converse = method

if hasattr(method, 'converse_intents'):
for intent_file in getattr(method, 'converse_intents'):
self.register_converse_intent(intent_file, method)

def _upload_settings(self):
"""
Upload settings to a remote backend if configured.
Expand Down Expand Up @@ -1111,6 +1164,13 @@ def _handle_converse_request(self, message: Message):
if message.data.get("skill_id") != self.skill_id:
return # not for us!

# check if a conversational intent triggered
# these are skill specific intents that may trigger instead of converse
if self._handle_converse_intents(message):
self.bus.emit(message.reply('skill.converse.response',
{"skill_id": self.skill_id, "result": True}))
return

try:
# converse can have multiple signatures
params = signature(self.converse).parameters
Expand Down
Loading