From 9db6596a62f11447a24a9858ba26d5181b86b13f Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Tue, 19 Aug 2025 13:41:46 -0700 Subject: [PATCH 1/2] Implement updated Skill API Implements updates to the Skill API allowing the method to specify input/return values as Pydantic models. Also implements standard exception handling, reporting errors back to a caller instead of sending no return message. --- ovos_workshop/skills/ovos.py | 59 ++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/ovos_workshop/skills/ovos.py b/ovos_workshop/skills/ovos.py index a232439a..51942739 100644 --- a/ovos_workshop/skills/ovos.py +++ b/ovos_workshop/skills/ovos.py @@ -1023,16 +1023,32 @@ def _register_public_api(self): messagebus handler for fetching the api info if any handlers exist. """ - def wrap_method(fn): + def wrap_method(fn, arg_model=None): """Boilerplate for returning the response to the sender.""" def wrapper(message): - result = fn(*message.data['args'], **message.data['kwargs']) + 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})) - + self.bus.emit(message.response(data={'result': result, + 'error': error})) 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__')] @@ -1042,10 +1058,40 @@ def wrapper(message): 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 + '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 @@ -1056,8 +1102,9 @@ def wrapper(message): # 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), speak_errors=False) + wrap_method(func, req_class), speak_errors=False) if self.public_api: self.add_event(f'{self.skill_id}.public_api', From 199c05f6f7a0dcad47bfeb2b0f6e1c74aa1101cf Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:28:53 -0700 Subject: [PATCH 2/2] Implement CodeRabbit suggested changes Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- ovos_workshop/skills/ovos.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/ovos_workshop/skills/ovos.py b/ovos_workshop/skills/ovos.py index 51942739..c819cb9c 100644 --- a/ovos_workshop/skills/ovos.py +++ b/ovos_workshop/skills/ovos.py @@ -1061,25 +1061,33 @@ def wrapper(message): # Extract method signature and return type import inspect - signature = inspect.signature(method) + sig = inspect.signature(method) schema = None return_schema = None request_class = None try: from pydantic import BaseModel - parameters = signature.parameters + parameters = sig.parameters for arg_name, param in parameters.items(): if arg_name == 'self': continue - if issubclass(param.annotation, BaseModel): + ann = param.annotation + if isinstance(ann, type) and issubclass(ann, BaseModel): # Get the JSON schema for the BaseModel - schema = param.annotation.model_json_schema() - request_class = param.annotation + try: + schema = ann.model_json_schema() + except AttributeError: + schema = ann.schema() + request_class = ann break - if signature.return_annotation and issubclass(signature.return_annotation, BaseModel): + ra = sig.return_annotation + if isinstance(ra, type) and issubclass(ra, BaseModel): # Get the JSON schema for the return type - return_schema = signature.return_annotation.model_json_schema() + try: + return_schema = ra.model_json_schema() + except AttributeError: + return_schema = ra.schema() except ImportError: # If pydantic is not installed, there is no schema to extract pass