From 130cd192d0882a920afb1eeca7b48d4452982d1a Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Tue, 19 Aug 2025 13:28:36 -0700 Subject: [PATCH 1/4] Implement `_register_public_api` with additional API method metadata --- neon_utils/skills/neon_skill.py | 98 +++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/neon_utils/skills/neon_skill.py b/neon_utils/skills/neon_skill.py index 8197d486..dc1c0ae4 100644 --- a/neon_utils/skills/neon_skill.py +++ b/neon_utils/skills/neon_skill.py @@ -227,6 +227,104 @@ def update_profile(self, new_preferences: dict, message: Message = None): except Exception as x: LOG.error(x) + # Duplicated from OVOSSkill for backwards-compat with skills using ovos-workshop 0.X + def _register_public_api(self): + """ + Find and register API methods decorated with `@api_method` and create a + messagebus handler for fetching the api info if any handlers exist. + """ + + def wrap_method(fn, arg_model=None): + """Boilerplate for returning the response to the sender.""" + + def wrapper(message): + start_time = time() + result = None + error = None + try: + if arg_model: + result = fn(arg_model(*message.data['args'], + **message.data['kwargs'])) + else: + result = fn(*message.data.get('args', []), + **message.data.get('kwargs', {})) + try: + result = result.model_dump() + except AttributeError: + # Response is not a Pydantic model + pass + except Exception as e: + error = str(e) + message.context["skill_id"] = self.skill_id + self.bus.emit(message.response(data={'result': result, + 'error': error})) + LOG.info(f"API method completed in {time() - start_time}s") + return wrapper + + from ovos_utils.skills import get_non_properties + 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__ + + # Extract method signature and return type + import inspect + signature = inspect.signature(method) + schema = None + return_schema = None + request_class = None + try: + from pydantic import BaseModel + parameters = signature.parameters + + for arg_name, param in parameters.items(): + if arg_name == 'self': + continue + if issubclass(param.annotation, BaseModel): + # Get the JSON schema for the BaseModel + schema = param.annotation.model_json_schema() + request_class = param.annotation + break + if signature.return_annotation and issubclass(signature.return_annotation, BaseModel): + # Get the JSON schema for the return type + return_schema = signature.return_annotation.model_json_schema() + except ImportError: + # If pydantic is not installed, there is no schema to extract + pass + + self.public_api[name] = { + 'help': doc, + 'type': f'{self.skill_id}.{name}', + 'func': method, + 'signature': str(signature), + 'request_schema': schema, + 'response_schema': return_schema, + 'request_class': request_class + } + for key in self.public_api: + if ('type' in self.public_api[key] and + 'func' in self.public_api[key]): + self.log.debug(f"Adding api method: " + f"{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') + req_class = self.public_api[key].pop('request_class', None) + self.add_event(self.public_api[key]['type'], + wrap_method(func, req_class), speak_errors=False) + + if self.public_api: + # TODO: Think about always registering this, so queries get an + # empty response, rather than waiting for a timeout + self.add_event(f'{self.skill_id}.public_api', + self._send_public_api, speak_errors=False) + @resolve_message def update_skill_settings(self, new_preferences: dict, message: Message = None, skill_global=True): From 2df61c3cacfd2a71a6d289b177e01cbd898fafa3 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Tue, 19 Aug 2025 13:51:35 -0700 Subject: [PATCH 2/4] Remove `packages-exclude` from license tests to troubleshoot failure --- .github/workflows/license_tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/license_tests.yml b/.github/workflows/license_tests.yml index 011f7454..cc773f98 100644 --- a/.github/workflows/license_tests.yml +++ b/.github/workflows/license_tests.yml @@ -11,4 +11,3 @@ jobs: uses: neongeckocom/.github/.github/workflows/license_tests.yml@master with: package-extras: audio,configuration,networking - packages-exclude: '^(precise-runner|fann2|tqdm|bs4|ovos-phal-plugin|ovos-skill|neon-core|nvidia|neon-phal-plugin|bitstruct|audioread|RapidFuzz|click|setuptools|typing_extensions|urllib).*' \ No newline at end of file From cf19785faff632eb3281f89585c95d0e85cb4a6b Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Tue, 19 Aug 2025 13:59:42 -0700 Subject: [PATCH 3/4] Update license tests to address test failure --- .github/workflows/license_tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/license_tests.yml b/.github/workflows/license_tests.yml index cc773f98..7adc134e 100644 --- a/.github/workflows/license_tests.yml +++ b/.github/workflows/license_tests.yml @@ -11,3 +11,4 @@ jobs: uses: neongeckocom/.github/.github/workflows/license_tests.yml@master with: package-extras: audio,configuration,networking + packages-exclude: '^(precise-runner|fann2|tqdm|bs4|ovos-phal-plugin|ovos-skill|neon-core|nvidia|neon-phal-plugin|bitstruct|audioread|RapidFuzz|click|setuptools|typing_extensions|urllib|marisa-trie).*' From 99fd85faf1cd748db6acc66c79d358da42930c5a Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 20 Aug 2025 14:08:30 -0700 Subject: [PATCH 4/4] Remove bad `time` reference and extra API method log --- neon_utils/skills/neon_skill.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/neon_utils/skills/neon_skill.py b/neon_utils/skills/neon_skill.py index dc1c0ae4..b9ce3c66 100644 --- a/neon_utils/skills/neon_skill.py +++ b/neon_utils/skills/neon_skill.py @@ -238,7 +238,6 @@ def wrap_method(fn, arg_model=None): """Boilerplate for returning the response to the sender.""" def wrapper(message): - start_time = time() result = None error = None try: @@ -258,7 +257,6 @@ def wrapper(message): message.context["skill_id"] = self.skill_id self.bus.emit(message.response(data={'result': result, 'error': error})) - LOG.info(f"API method completed in {time() - start_time}s") return wrapper from ovos_utils.skills import get_non_properties