Skip to content
Open
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
22 changes: 19 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
# Changelog

## [7.0.6a1](https://github.com/OpenVoiceOS/ovos-workshop/tree/7.0.6a1) (2025-06-16)
## [7.0.9a1](https://github.com/OpenVoiceOS/ovos-workshop/tree/7.0.9a1) (2025-07-08)

[Full Changelog](https://github.com/OpenVoiceOS/ovos-workshop/compare/7.0.5...7.0.6a1)
[Full Changelog](https://github.com/OpenVoiceOS/ovos-workshop/compare/7.0.8a1...7.0.9a1)

**Merged pull requests:**

- fix: on\_event\_start\_wrapper [\#360](https://github.com/OpenVoiceOS/ovos-workshop/pull/360) ([JarbasAl](https://github.com/JarbasAl))
- fix: ocp skills [\#365](https://github.com/OpenVoiceOS/ovos-workshop/pull/365) ([JarbasAl](https://github.com/JarbasAl))

## [7.0.8a1](https://github.com/OpenVoiceOS/ovos-workshop/tree/7.0.8a1) (2025-07-08)

[Full Changelog](https://github.com/OpenVoiceOS/ovos-workshop/compare/7.0.7a1...7.0.8a1)

**Merged pull requests:**

- fix: rm \_\_del\_\_ method [\#368](https://github.com/OpenVoiceOS/ovos-workshop/pull/368) ([JarbasAl](https://github.com/JarbasAl))

## [7.0.7a1](https://github.com/OpenVoiceOS/ovos-workshop/tree/7.0.7a1) (2025-06-21)

[Full Changelog](https://github.com/OpenVoiceOS/ovos-workshop/compare/7.0.6...7.0.7a1)

**Merged pull requests:**

- refactor/remove the compatibility layer with MycroftSkill in the skill launcher [\#362](https://github.com/OpenVoiceOS/ovos-workshop/pull/362) ([JarbasAl](https://github.com/JarbasAl))



Expand Down
70 changes: 35 additions & 35 deletions downstream_report.txt
Original file line number Diff line number Diff line change
@@ -1,46 +1,46 @@
ovos-workshop==7.0.2
├── ovos-plugin-common-play==1.2.1 [requires: ovos-workshop>=2.4.2,<8.0.0]
ovos-workshop==7.0.7a1
├── ovos-skill-application-launcher==0.5.14 [requires: ovos-workshop>=6.0.0,<8.0.0]
├── ovos-skill-date-time==1.1.4 [requires: ovos-workshop>=0.0.16,<8.0.0]
├── ovos-skill-ip==0.2.8 [requires: ovos-workshop]
├── ovos-skill-screenshot==0.0.7 [requires: ovos-workshop]
├── ovos-skill-naptime==0.3.15 [requires: ovos-workshop>=0.0.15,<8.0.0]
├── ovos-skill-volume==0.1.16 [requires: ovos-workshop>=0.0.15,<8.0.0]
├── ovos-core==2.0.2a1 [requires: ovos-workshop>=7.0.2,<8.0.0]
├── ovos-skill-confucius-quotes==0.1.13 [requires: ovos-workshop>=0.0.15,<8.0.0]
├── ovos-skill-word-of-the-day==0.2.0 [requires: ovos-workshop]
├── ovos-skill-days-in-history==0.3.11 [requires: ovos-workshop>=3.1.0,<8.0.0]
├── ovos-skill-weather==1.0.5a1 [requires: ovos-workshop>=2.2.0,<8.0.0]
├── ovos-skill-speedtest==0.3.6 [requires: ovos-workshop>=0.0.12,<8.0.0]
├── ovos-skill-fallback-unknown==0.1.9 [requires: ovos-workshop>=6.0.0,<8.0.0]
├── ovos-ocp-pipeline-plugin==1.1.16 [requires: ovos-workshop>=0.1.7,<8.0.0]
├── ovos-skill-ddg==0.3.5 [requires: ovos-workshop>=3.4.0,<8.0.0]
├── ovos-skill-alerts==0.1.27 [requires: ovos-workshop>=7.0.0,<8.0.0]
├── ovos-skill-diagnostics==0.0.8 [requires: ovos-workshop>=0.0.12]
├── ovos-skill-fuster-quotes==0.0.4 [requires: ovos-workshop]
├── ovos-adapt-parser==1.0.8 [requires: ovos-workshop>=0.1.7,<8.0.0]
├── ovos-skill-weather==1.0.5a1 [requires: ovos-workshop>=2.2.0,<8.0.0]
├── ovos-common-query-pipeline-plugin==1.1.8 [requires: ovos-workshop>=0.1.7,<8.0.0]
├── ovos-m2v-pipeline==0.0.6 [requires: ovos-workshop>=0.1.7,<8.0.0]
├── ovos-skill-screenshot==0.0.7 [requires: ovos-workshop]
├── ovos-skill-moviemaster==0.0.12 [requires: ovos-workshop>=0.0.11,<8.0.0]
├── ovos-skill-camera==1.0.5a3 [requires: ovos-workshop>=0.0.12]
├── ovos-skill-wikihow==0.3.3 [requires: ovos-workshop>=3.4.0a1,<8.0.0]
├── ovos-skill-wolfie==0.5.8 [requires: ovos-workshop>=3.4.0a1,<8.0.0]
├── ovos-core==2.0.4a4 [requires: ovos-workshop>=7.0.6,<8.0.0]
├── ovos-skill-number-facts==0.1.12 [requires: ovos-workshop>=0.0.15,<8.0.0]
├── ovos-skill-homescreen==3.0.3 [requires: ovos-workshop>=2.4.0,<8.0.0]
├── ovos-common-query-pipeline-plugin==1.1.8 [requires: ovos-workshop>=0.1.7,<8.0.0]
├── ovos-skill-ddg==0.3.6a1 [requires: ovos-workshop>=3.4.0,<8.0.0]
├── ovos-skill-iss-location==0.2.16 [requires: ovos-workshop>=0.0.12,<8.0.0]
├── ovos-padatious==1.4.2 [requires: ovos-workshop>=0.1.7,<8.0.0]
├── ovos-skill-fallback-unknown==0.1.9 [requires: ovos-workshop>=6.0.0,<8.0.0]
├── ovos-skill-somafm==0.1.6a1 [requires: ovos-workshop>=0.0.16]
├── ovos-skill-news==0.4.5 [requires: ovos-workshop>=0.0.16,<8.0.0]
├── ovos-skill-pyradios==0.1.5 [requires: ovos-workshop>=0.0.16,<8.0.0]
├── ovos-skill-wordnet==0.2.5 [requires: ovos-workshop>=3.3.0,<8.0.0]
├── ovos-skill-laugh==0.2.3 [requires: ovos-workshop]
├── ovos-skill-diagnostics==0.0.8 [requires: ovos-workshop>=0.0.12]
├── ovos-skill-icanhazdadjokes==0.3.7 [requires: ovos-workshop>=0.0.15,<8.0.0]
├── ovos-skill-iss-location==0.2.16 [requires: ovos-workshop>=0.0.12,<8.0.0]
├── ovos-skill-wolfie==0.5.8 [requires: ovos-workshop>=3.4.0a1,<8.0.0]
├── ovos-skill-speedtest==0.3.6 [requires: ovos-workshop>=0.0.12,<8.0.0]
├── ovos-skill-local-media==0.2.12 [requires: ovos-workshop>=2.4.0,<8.0.0]
├── ovos-skill-laugh==0.2.3 [requires: ovos-workshop]
├── ovos-skill-color-picker==0.0.7 [requires: ovos-workshop]
├── ovos-skill-wikipedia==0.8.13 [requires: ovos-workshop>=3.4.0,<8.0.0]
├── ovos-skill-randomness==0.1.2a1 [requires: ovos-workshop]
├── ovos-skill-moviemaster==0.0.12 [requires: ovos-workshop>=0.0.11,<8.0.0]
├── ovos-skill-date-time==1.1.4 [requires: ovos-workshop>=0.0.16,<8.0.0]
├── ovos-skill-word-of-the-day==0.2.0 [requires: ovos-workshop]
├── ovos-skill-confucius-quotes==0.1.13 [requires: ovos-workshop>=0.0.15,<8.0.0]
├── ovos-skill-wordnet==0.2.6a1 [requires: ovos-workshop>=3.3.0,<8.0.0]
├── ovos-skill-parrot==0.1.25 [requires: ovos-workshop>=7.0.0,<8.0.0]
├── ovos-skill-youtube-music==0.1.7 [requires: ovos-workshop>=0.0.16,<8.0.0]
├── ovos-skill-cmd==0.2.11 [requires: ovos-workshop>=0.0.15,<8.0.0]
├── ovos-skill-personal==0.1.19 [requires: ovos-workshop>=0.0.15,<8.0.0]
├── ovos-skill-local-media==0.2.12 [requires: ovos-workshop>=2.4.0,<8.0.0]
├── ovos-skill-alerts==0.1.27 [requires: ovos-workshop>=7.0.0,<8.0.0]
├── ovos-adapt-parser==1.0.8 [requires: ovos-workshop>=0.1.7,<8.0.0]
├── ovos-skill-youtube-music==0.1.7 [requires: ovos-workshop>=0.0.16,<8.0.0]
├── ovos-skill-volume==0.1.16 [requires: ovos-workshop>=0.0.15,<8.0.0]
├── ovos-skill-news==0.4.5 [requires: ovos-workshop>=0.0.16,<8.0.0]
├── ovos-ocp-pipeline-plugin==1.1.16 [requires: ovos-workshop>=0.1.7,<8.0.0]
├── ovos-skill-camera==1.0.5a3 [requires: ovos-workshop>=0.0.12]
├── ovos-plugin-common-play==1.2.1 [requires: ovos-workshop>=2.4.2,<8.0.0]
├── ovos-skill-homescreen==3.0.3 [requires: ovos-workshop>=2.4.0,<8.0.0]
├── ovos-skill-pyradios==0.1.5 [requires: ovos-workshop>=0.0.16,<8.0.0]
├── ovos-skill-dictation==0.2.19 [requires: ovos-workshop>=7.0.0,<8.0.0]
├── ovos-skill-color-picker==0.0.7 [requires: ovos-workshop]
├── ovos-padatious==1.4.2 [requires: ovos-workshop>=0.1.7,<8.0.0]
├── ovos-skill-ip==0.2.8 [requires: ovos-workshop]
└── ovos-skill-wikihow==0.3.3 [requires: ovos-workshop>=3.4.0a1,<8.0.0]
├── ovos-skill-randomness==0.1.2 [requires: ovos-workshop]
├── ovos-skill-days-in-history==0.3.11 [requires: ovos-workshop>=3.1.0,<8.0.0]
└── ovos-skill-cmd==0.2.11 [requires: ovos-workshop>=0.0.15,<8.0.0]
164 changes: 61 additions & 103 deletions ovos_workshop/skill_launcher.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import gc
import os
import sys
from os.path import isdir
Expand All @@ -13,7 +12,7 @@
from ovos_plugin_manager.skills import find_skill_plugins, get_skill_directories
from ovos_utils import wait_for_exit_signal
from ovos_utils.file_utils import FileWatcher
from ovos_utils.log import LOG, log_deprecation
from ovos_utils.log import LOG
from ovos_utils.process_utils import RuntimeRequirements

from ovos_workshop.skills.active import ActiveSkill
Expand All @@ -22,10 +21,11 @@
from ovos_workshop.skills.common_query_skill import CommonQuerySkill
from ovos_workshop.skills.fallback import FallbackSkill
from ovos_workshop.skills.ovos import OVOSSkill
from ovos_workshop.skills.game_skill import OVOSGameSkill, ConversationalGameSkill

SKILL_BASE_CLASSES = [
OVOSSkill, OVOSCommonPlaybackSkill, CommonQuerySkill, ActiveSkill,
FallbackSkill, UniversalSkill, UniversalFallback
FallbackSkill, UniversalSkill, UniversalFallback, OVOSGameSkill, ConversationalGameSkill
]

SKILL_MAIN_MODULE = '__init__.py'
Expand Down Expand Up @@ -118,22 +118,6 @@ def get_skill_class(skill_module: ModuleType) -> Optional[callable]:
return None


def get_create_skill_function(skill_module: ModuleType) -> Optional[callable]:
"""Find create_skill function in skill module.

Arguments:
skill_module (module): module to search for create_skill function

Returns:
(function): Found create_skill function or None.
"""
if hasattr(skill_module, "create_skill") and \
callable(skill_module.create_skill):
log_deprecation("`create_skill` method is no longer supported", "0.1.0")
return skill_module.create_skill
return None


class SkillLoader:
def __init__(self, bus: MessageBusClient,
skill_directory: Optional[str] = None,
Expand Down Expand Up @@ -280,20 +264,20 @@ def load(self, _=None) -> bool:

def _unload(self):
"""
Remove listeners and stop threads before loading
Performs cleanup by stopping file watchers, emitting a skill shutdown event, and shutting down the skill instance.
"""
if self._watchdog:
self._watchdog.shutdown()
self._watchdog = None

if self.bus:
message = Message("mycroft.skills.shutdown",
{"path": self.skill_directory, "id": self.skill_id})
self.bus.emit(message)
self._execute_instance_shutdown()
if self.config.get("debug", False):
self._garbage_collect()
self._emit_skill_shutdown_event()

def unload(self):
"""
Shutdown and unload the skill instance
Shuts down and unloads the skill instance if it is currently loaded.
"""
if self.instance:
self._execute_instance_shutdown()
Expand All @@ -314,7 +298,9 @@ def deactivate(self):

def _execute_instance_shutdown(self):
"""
Call the shutdown method of the skill being reloaded.
Invokes shutdown routines on the current skill instance and handles any exceptions.

Calls both `shutdown()` and `default_shutdown()` methods on the skill instance if present, logging any exceptions that occur. Cleans up the instance reference after shutdown.
"""
if self.instance:
try:
Expand All @@ -329,27 +315,6 @@ def _execute_instance_shutdown(self):
del self.instance
self.instance = None

def _garbage_collect(self):
"""
Invoke Python garbage collector to remove false references
"""
gc.collect()
# Remove two local references that are known
refs = sys.getrefcount(self.instance) - 2
if refs > 0:
LOG.warning(
f"After shutdown of {self.skill_id} there are still {refs} "
f"references remaining. The skill won't be cleaned from memory."
)

def _emit_skill_shutdown_event(self):
"""
Emit `mycroft.skills.shutdown` to notify the skill is being shutdown
"""
message = Message("mycroft.skills.shutdown",
{"path": self.skill_directory, "id": self.skill_id})
self.bus.emit(message)

def _load(self) -> bool:
"""
Load the skill if it is not blacklisted, emit load status, start file
Expand Down Expand Up @@ -411,80 +376,54 @@ def _skip_load(self):

def _load_skill_source(self) -> ModuleType:
"""
Use Python's import library to load a skill's source code.
@return: Skill module to instantiate
Loads the main skill module from the skill directory using Python's import system.

Returns:
ModuleType: The loaded skill module.

Raises:
FileNotFoundError: If the main skill file does not exist in the skill directory.
"""
main_file_path = os.path.join(self.skill_directory, SKILL_MAIN_MODULE)
skill_module = None
if not os.path.exists(main_file_path):
LOG.error(f'Failed to load {self.skill_id} due to a missing file.')
raise FileNotFoundError(f"Failed to load '{self.skill_id}' - expected file: {main_file_path}")
else:
try:
skill_module = load_skill_module(main_file_path, self.skill_id)
except Exception as e:
LOG.exception(f'Failed to load skill: {self.skill_id} ({e})')
return skill_module

def _create_skill_instance(self,
skill_module: Optional[ModuleType] = None) -> \
bool:
def _create_skill_instance(self, skill_module: Optional[ModuleType] = None) -> bool:
"""
Create the skill object.

Arguments:
skill_module (module): Module to load from

Instantiate the skill class.

Attempts to create the skill instance from the provided module or the loader's skill module. If a suitable skill class is found, it is instantiated with the message bus and skill ID. Returns True if the skill instance was created successfully, otherwise False.

Parameters:
skill_module (ModuleType, optional): The module from which to load the skill class or creation function.

Returns:
(bool): True if skill was loaded successfully.
bool: True if the skill instance was created successfully, False otherwise.
"""
skill_module = skill_module or self.skill_module
skill_creator = None
if skill_module:
try:
# in skill classes __new__ should fully create the skill object
skill_class = get_skill_class(skill_module)
self.instance = skill_class(bus=self.bus, skill_id=self.skill_id)
return self.instance is not None
except Exception as e:
LOG.warning(f"Skill load raised exception: {e}")

try:
# attempt to use old style create_skill function entrypoint
skill_creator = get_create_skill_function(skill_module) or \
self.skill_class
except Exception as e:
LOG.exception(f"Failed to load skill creator: {e}")
self.instance = None
return False
LOG.debug(f"extracting skill class from: '{skill_module}'")
skill_class = get_skill_class(skill_module)
elif not self.skill_class and self.skill_module:
LOG.debug(f"extracting skill class from: '{self.skill_module}'")
skill_class = get_skill_class(self.skill_module)
else:
LOG.debug(f"explicitly provided skill class: '{skill_module}'")
skill_class = self.skill_class

if not skill_creator and self.skill_class:
skill_creator = self.skill_class

# if the signature supports skill_id and bus pass them
# to fully initialize the skill in 1 go
try:
# skills that do will have bus and skill_id available
# as soon as they call super()
self.instance = skill_creator(bus=self.bus,
skill_id=self.skill_id)
self.instance = skill_class(bus=self.bus, skill_id=self.skill_id)
except Exception as e:
# most old skills do not expose bus/skill_id kwargs
LOG.warning(f"Legacy skill: {e}")
self.instance = skill_creator()

if not self.instance.is_fully_initialized:
try:
# finish initialization of skill if we didn't manage to inject
# skill_id and bus kwargs.
# these skills only have skill_id and bus available in initialize,
# not in __init__
log_deprecation("This initialization is deprecated. Update skill to"
"handle passed `skill_id` and `bus` kwargs",
"0.1.0")
self.instance._startup(self.bus, self.skill_id)
except Exception as e:
LOG.exception(f'Skill __init__ failed with {e}')
self.instance = None
LOG.exception(f'Skill loading failed with {e}')
self.instance = None

return self.instance is not None

Expand Down Expand Up @@ -628,19 +567,37 @@ def load_skill(self, message: Optional[Message] = None):

def run(self):
"""
Connect to core and run until KeyboardInterrupt.
Connects to the core message bus and runs the skill container until interrupted.

Runs the main event loop, waiting for an exit signal or keyboard interruption. Upon exit, ensures the skill is properly unloaded.
"""
self._connect_to_core()
try:
wait_for_exit_signal()
except KeyboardInterrupt:
pass
self.unload()

def unload(self):
"""
Deactivates and unloads the skill if a skill loader is present.
"""
if self.skill_loader:
self.skill_loader.deactivate()
self.skill_loader._unload()

def __del__(self):
"""
Ensures the skill is properly unloaded when the SkillContainer is destroyed.
"""
self.unload()

def _launch_plugin_skill(self):
"""
Launch a skill plugin associated with this SkillContainer instance.
Launches the skill plugin corresponding to this SkillContainer's skill ID.

Raises:
ValueError: If the skill ID does not match any available skill plugin.
"""
plugins = find_skill_plugins()
if self.skill_id not in plugins:
Expand Down Expand Up @@ -669,6 +626,7 @@ def _launch_script():
Console script entrypoint
USAGE: ovos-skill-launcher {skill_id} [path/to/my/skill_id]
"""
LOG.set_level("DEBUG")
args_count = len(sys.argv)
if args_count == 2:
skill_id = sys.argv[1]
Expand Down
Loading