Skip to content
136 changes: 125 additions & 11 deletions neon_skill_fallback_wolfram_alpha/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,20 @@
# limitations under the License.


from typing import Tuple
from typing import Tuple, Literal
from ovos_utils import classproperty
from ovos_utils.log import LOG
from ovos_utils.process_utils import RuntimeRequirements
from ovos_bus_client.message import dig_for_message
from ovos_workshop.intents import IntentBuilder
from ovos_workshop.skills.common_query_skill import CommonQuerySkill, CQSMatchLevel
from ovos_workshop.decorators import skill_api_method
from lingua_franca.parse import normalize
from neon_utils.user_utils import get_message_user, get_user_prefs
from neon_utils.hana_utils import request_backend

from neon_skill_fallback_wolfram_alpha.data_models import WolframAlphaQuery, WolframAlphaResponse


class WolframAlphaSkill(CommonQuerySkill):
def __init__(self, **kwargs):
Expand Down Expand Up @@ -135,6 +138,24 @@ def handle_get_sources(self, message):
else:
self.speak_dialog("no.info.to.send", private=True)

@skill_api_method
def get_wolfram_response(self, request: WolframAlphaQuery) -> WolframAlphaResponse:
"""
Get a response from WolframAlpha for a given query and location. Any
application must include "Powered by Wolfram|Alpha" in the response to
comply with WolframAlpha's attribution requirements.
@param request: The request object to send to WolframAlpha.
@return: The response from WolframAlpha
"""
try:
result = request_backend("proxy/wolframalpha",
request.model_dump())
except Exception as e:
LOG.error(e)
result = {}
LOG.info(f"result={result}")
return WolframAlphaResponse(**result)

def _query_wolfram(self, utterance, message) -> Tuple[str, str]:
query = normalize(utterance, remove_articles=False)
# TODO: Better parsing of utterance into a question
Expand All @@ -144,15 +165,108 @@ def _query_wolfram(self, utterance, message) -> Tuple[str, str]:
lat = str(preference_location['lat'])
lng = str(preference_location['lng'])
units = str(get_user_prefs(message)["units"]["measure"])
units = "metric" if units == "metric" else "nonmetric"
query_type = "short" if message.context.get("klat_data") else "spoken"
key = (query, lat, lng, units, query_type)
kwargs = {"lat": lat, "lon": lng, "api": query_type, "units": units,
"query": query}
try:
result = request_backend("proxy/wolframalpha",
kwargs).get("answer")
except Exception as e:
LOG.error(e)
result = None
LOG.info(f"result={result}")
return result, key
resp = self.get_wolfram_response(WolframAlphaQuery(query=query,
lat=lat,
lon=lng,
units=units,
api=query_type)).answer
return resp, key

# 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):
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}))
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)
43 changes: 43 additions & 0 deletions neon_skill_fallback_wolfram_alpha/data_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
# All trademark and other rights reserved by their respective owners
# Copyright 2008-2026 Neongecko.com Inc.
# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds,
# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
# BSD-3 License
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from typing import Literal
from pydantic import BaseModel, Field

WA_API = Literal["short", "spoken"]

class WolframAlphaQuery(BaseModel):
query: str = Field(description="The query to send to Wolfram Alpha")
lat: float = Field(description="User location latitude")
lon: float = Field(description="User location longitude")
units: Literal["metric", "nonmetric"] = Field(description="Units to use for the query", default="metric")
api: WA_API = Field(description="Wolfram Alpha API to use for the query", default="short")


class WolframAlphaResponse(BaseModel):
answer: str = Field(description="The answer to the query")
2 changes: 1 addition & 1 deletion skill.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"license": "BSD-3-Clause",
"author": "Neongecko",
"tags": [],
"version": "3.0.0",
"version": "3.0.1a1",
"url": "https://github.com/NeonGeckoCom/skill-fallback_wolfram_alpha",
"skill_id": "skill-fallback_wolfram_alpha.neongeckocom=neon_skill_fallback_wolfram_alpha:WolframAlphaSkill",
"skillname": "skill-fallback_wolfram_alpha",
Expand Down