Skip to content

Commit ea698cd

Browse files
authored
Update Skill API to support outside applications (#554)
* Implement `_register_public_api` with additional API method metadata * Remove `packages-exclude` from license tests to troubleshoot failure * Update license tests to address test failure * Remove bad `time` reference and extra API method log
1 parent 9dc3f32 commit ea698cd

2 files changed

Lines changed: 97 additions & 1 deletion

File tree

.github/workflows/license_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ jobs:
1111
uses: neongeckocom/.github/.github/workflows/license_tests.yml@master
1212
with:
1313
package-extras: audio,configuration,networking
14-
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).*'
14+
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).*'

neon_utils/skills/neon_skill.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,102 @@ def update_profile(self, new_preferences: dict, message: Message = None):
227227
except Exception as x:
228228
LOG.error(x)
229229

230+
# Duplicated from OVOSSkill for backwards-compat with skills using ovos-workshop 0.X
231+
def _register_public_api(self):
232+
"""
233+
Find and register API methods decorated with `@api_method` and create a
234+
messagebus handler for fetching the api info if any handlers exist.
235+
"""
236+
237+
def wrap_method(fn, arg_model=None):
238+
"""Boilerplate for returning the response to the sender."""
239+
240+
def wrapper(message):
241+
result = None
242+
error = None
243+
try:
244+
if arg_model:
245+
result = fn(arg_model(*message.data['args'],
246+
**message.data['kwargs']))
247+
else:
248+
result = fn(*message.data.get('args', []),
249+
**message.data.get('kwargs', {}))
250+
try:
251+
result = result.model_dump()
252+
except AttributeError:
253+
# Response is not a Pydantic model
254+
pass
255+
except Exception as e:
256+
error = str(e)
257+
message.context["skill_id"] = self.skill_id
258+
self.bus.emit(message.response(data={'result': result,
259+
'error': error}))
260+
return wrapper
261+
262+
from ovos_utils.skills import get_non_properties
263+
methods = [attr_name for attr_name in get_non_properties(self)
264+
if hasattr(getattr(self, attr_name), '__name__')]
265+
266+
for attr_name in methods:
267+
method = getattr(self, attr_name)
268+
269+
if hasattr(method, 'api_method'):
270+
doc = method.__doc__ or ''
271+
name = method.__name__
272+
273+
# Extract method signature and return type
274+
import inspect
275+
signature = inspect.signature(method)
276+
schema = None
277+
return_schema = None
278+
request_class = None
279+
try:
280+
from pydantic import BaseModel
281+
parameters = signature.parameters
282+
283+
for arg_name, param in parameters.items():
284+
if arg_name == 'self':
285+
continue
286+
if issubclass(param.annotation, BaseModel):
287+
# Get the JSON schema for the BaseModel
288+
schema = param.annotation.model_json_schema()
289+
request_class = param.annotation
290+
break
291+
if signature.return_annotation and issubclass(signature.return_annotation, BaseModel):
292+
# Get the JSON schema for the return type
293+
return_schema = signature.return_annotation.model_json_schema()
294+
except ImportError:
295+
# If pydantic is not installed, there is no schema to extract
296+
pass
297+
298+
self.public_api[name] = {
299+
'help': doc,
300+
'type': f'{self.skill_id}.{name}',
301+
'func': method,
302+
'signature': str(signature),
303+
'request_schema': schema,
304+
'response_schema': return_schema,
305+
'request_class': request_class
306+
}
307+
for key in self.public_api:
308+
if ('type' in self.public_api[key] and
309+
'func' in self.public_api[key]):
310+
self.log.debug(f"Adding api method: "
311+
f"{self.public_api[key]['type']}")
312+
313+
# remove the function member since it shouldn't be
314+
# reused and can't be sent over the messagebus
315+
func = self.public_api[key].pop('func')
316+
req_class = self.public_api[key].pop('request_class', None)
317+
self.add_event(self.public_api[key]['type'],
318+
wrap_method(func, req_class), speak_errors=False)
319+
320+
if self.public_api:
321+
# TODO: Think about always registering this, so queries get an
322+
# empty response, rather than waiting for a timeout
323+
self.add_event(f'{self.skill_id}.public_api',
324+
self._send_public_api, speak_errors=False)
325+
230326
@resolve_message
231327
def update_skill_settings(self, new_preferences: dict,
232328
message: Message = None, skill_global=True):

0 commit comments

Comments
 (0)