Skip to content
This repository has been archived by the owner on Sep 8, 2024. It is now read-only.

Feature/skill api #1822

Merged
merged 4 commits into from
Feb 23, 2021
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
68 changes: 68 additions & 0 deletions mycroft/client/text/text_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,7 @@ def make_titlebar(title, bar_length):
"Skill Debugging Commands",
[
(":skills", "list installed skills"),
(":api SKILL", "show skill's public API"),
(":activate SKILL", "activate skill, e.g. 'activate skill-wiki'"),
(":deactivate SKILL", "deactivate skill"),
(":keep SKILL", "deactivate all skills except " +
Expand Down Expand Up @@ -1035,6 +1036,62 @@ def prepare_page():
scr.refresh()


def show_skill_api(skill, data):
"""Show available help on skill's API."""
global scr
global screen_mode

if not scr:
return

screen_mode = SCR_SKILLS

row = 2
column = 0

def prepare_page():
global scr
nonlocal row
nonlocal column
scr.erase()
scr.addstr(0, 0, center(25) + "Skill-API for {}".format(skill),
CLR_CMDLINE)
scr.addstr(1, 1, "=" * (curses.COLS - 2), CLR_CMDLINE)
row = 2
column = 4

prepare_page()
for key in data:
color = curses.color_pair(4)

scr.addstr(row, column, "{} ({})".format(key, data[key]['type']),
CLR_HEADING)
row += 2
if 'help' in data[key]:
help_text = data[key]['help'].split('\n')
for line in help_text:
scr.addstr(row, column + 2, line, color)
row += 1
row += 2
else:
row += 1

if row == curses.LINES - 5:
scr.addstr(curses.LINES - 1, 0,
center(23) + "Press any key to continue", CLR_HEADING)
scr.refresh()
wait_for_any_key()
prepare_page()
elif row == curses.LINES - 5:
# Reached bottom of screen, start at top and move output to a
# New column
row = 2

scr.addstr(curses.LINES - 1, 0, center(23) + "Press any key to return",
CLR_HEADING)
scr.refresh()


def center(str_len):
# generate number of characters needed to center a string
# of the given length
Expand Down Expand Up @@ -1182,6 +1239,17 @@ def handle_cmd(cmd):
bus.emit(Message("skillmanager.activate", data={'skill': s}))
else:
add_log_message('Usage :activate SKILL [SKILL2] [...]')
elif "api" in cmd:
parts = cmd.split()
if len(parts) < 2:
return
skill = parts[1]
message = bus.wait_for_response(Message('{}.public_api'.format(skill)))
if message:
show_skill_api(skill, message.data)
scr.get_wch() # blocks
screen_mode = SCR_MAIN
set_screen_dirty()

# TODO: More commands
return 0 # do nothing upon return
Expand Down
2 changes: 1 addition & 1 deletion mycroft/skills/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@


from .mycroft_skill import (MycroftSkill, intent_handler, intent_file_handler,
resting_screen_handler)
resting_screen_handler, skill_api_method)
from .fallback_skill import FallbackSkill
from .common_iot_skill import CommonIoTSkill
from .common_play_skill import CommonPlaySkill, CPSMatchLevel
Expand Down
3 changes: 3 additions & 0 deletions mycroft/skills/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
from mycroft.util.lang import set_active_lang
from mycroft.util.log import LOG
from mycroft.util.process_utils import ProcessStatus, StatusCallbackMap

from .api import SkillApi
from .core import FallbackSkill
from .event_scheduler import EventScheduler
from .intent_service import IntentService
Expand Down Expand Up @@ -211,6 +213,7 @@ def main(alive_hook=on_alive, started_hook=on_started, ready_hook=on_ready,
on_stopping=stopping_hook)
status = ProcessStatus('skills', bus, callbacks)

SkillApi.connect_bus(bus)
skill_manager = _initialize_skill_manager(bus, watchdog)

status.set_started()
Expand Down
67 changes: 67 additions & 0 deletions mycroft/skills/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copyright 2020 Mycroft AI Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Skill Api

The skill api allows skills interact with eachother over the message bus
just like interacting with any other object.
"""
from mycroft.messagebus.message import Message


class SkillApi():
"""SkillApi providing a simple interface to exported methods from skills

Methods are built from a method_dict provided when initializing the skill.
"""
bus = None

@classmethod
def connect_bus(cls, mycroft_bus):
"""Registers the bus object to use."""
cls.bus = mycroft_bus

def __init__(self, method_dict):
self.method_dict = method_dict
for key in method_dict:
def get_method(k):
def method(*args, **kwargs):
m = self.method_dict[k]
data = {'args': args, 'kwargs': kwargs}
method_msg = Message(m['type'], data)
response = SkillApi.bus.wait_for_response(method_msg)
if (response and response.data and
'result' in response.data):
return response.data['result']
else:
return None

return method

self.__setattr__(key, get_method(key))

@staticmethod
def get(skill):
"""Generate api object from skill id.
Arguments:
skill (str): skill id for target skill

Returns:
SkillApi
"""
public_api_msg = '{}.public_api'.format(skill)
api = SkillApi.bus.wait_for_response(Message(public_api_msg))
if api:
return SkillApi(api.data)
else:
return None
2 changes: 1 addition & 1 deletion mycroft/skills/mycroft_skill/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
from .mycroft_skill import MycroftSkill
from .event_container import get_handler_name
from .decorators import (intent_handler, intent_file_handler,
resting_screen_handler)
resting_screen_handler, skill_api_method)
12 changes: 12 additions & 0 deletions mycroft/skills/mycroft_skill/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,15 @@ def real_decorator(func):
return func

return real_decorator


def skill_api_method(func):
"""Decorator for adding a method to the skill's public api.

Methods with this decorator will be registered on the message bus
and an api object can be created for interaction with the skill.
"""
# tag the method by adding an api_method member to it
if not hasattr(func, 'api_method') and hasattr(func, '__name__'):
func.api_method = True
return func
56 changes: 56 additions & 0 deletions mycroft/skills/mycroft_skill/mycroft_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ def __init__(self, name=None, bus=None, use_settings=True):
self.event_scheduler = EventSchedulerInterface(self.name)
self.intent_service = IntentServiceInterface()

# Skill Public API
self.public_api = {}

def _init_settings(self):
"""Setup skill settings."""

Expand Down Expand Up @@ -258,6 +261,55 @@ def bind(self, bus):
# Initialize the SkillGui
self.gui.setup_default_handlers()

self._register_public_api()

def _register_public_api(self):
""" Find and register api methods.
Api methods has been tagged with the api_method member, for each
method where this is found the method a message bus handler is
registered.
Finally create a handler for fetching the api info from any requesting
skill.
"""

def wrap_method(func):
"""Boiler plate for returning the response to the sender."""
def wrapper(message):
result = func(*message.data['args'], **message.data['kwargs'])
self.bus.emit(message.response(data={'result': result}))

return wrapper

methods = [attr_name for attr_name in get_non_properties(self)
if hasattr(getattr(self, attr_name), '__name__')]

for attr_name in methods:
method = getattr(self, attr_name)

if hasattr(method, 'api_method'):
doc = method.__doc__ or ''
name = method.__name__
self.public_api[name] = {
'help': doc,
'type': '{}.{}'.format(self.skill_id, name),
'func': method
}
for key in self.public_api:
if ('type' in self.public_api[key] and
'func' in self.public_api[key]):
LOG.debug('Adding api method: '
'{}'.format(self.public_api[key]['type']))

# remove the function member since it shouldn't be
# reused and can't be sent over the messagebus
func = self.public_api[key].pop('func')
self.add_event(self.public_api[key]['type'],
wrap_method(func))

if self.public_api:
self.add_event('{}.public_api'.format(self.skill_id),
self._send_public_api)

def _register_system_event_handlers(self):
"""Add all events allowing the standard interaction with the Mycroft
system.
Expand Down Expand Up @@ -324,6 +376,10 @@ def initialize(self):
"""
pass

def _send_public_api(self, message):
"""Respond with the skill's public api."""
self.bus.emit(message.response(data=self.public_api))

def get_intro_message(self):
"""Get a message to speak on first load of the skill.

Expand Down
Loading