Skip to content

Commit

Permalink
Merge branch 'NeonGeckoCom:dev' into patch-1
Browse files Browse the repository at this point in the history
  • Loading branch information
NeonClary committed Jul 16, 2024
2 parents a257d68 + eab0844 commit 34da984
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 80 deletions.
104 changes: 69 additions & 35 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
# 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 enum import Enum
from threading import Thread
from time import time
Expand All @@ -34,27 +35,27 @@
from ovos_utils import classproperty
from ovos_utils.log import LOG
from ovos_utils.process_utils import RuntimeRequirements
# from ovos_workshop.skills.fallback import FallbackSkill
from neon_utils.skills.neon_fallback_skill import NeonFallbackSkill, NeonSkill
from neon_utils.message_utils import get_message_user
from ovos_workshop.skills.fallback import FallbackSkill
from ovos_workshop.decorators import intent_handler, fallback_handler
from neon_utils.message_utils import get_message_user, dig_for_message
from neon_utils.user_utils import get_user_prefs
from neon_utils.hana_utils import request_backend
from neon_mq_connector.utils.client_utils import send_mq_request

from mycroft.skills.mycroft_skill.decorators import intent_handler


class LLM(Enum):
GPT = "Chat GPT"
FASTCHAT = "FastChat"


class LLMSkill(NeonFallbackSkill):
def __init__(self, **kwargs):
NeonFallbackSkill.__init__(self, **kwargs)
class LLMSkill(FallbackSkill):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.chat_history = dict()
self._default_user = "local"
self._default_llm = LLM.FASTCHAT
self.chatting = dict()
self.register_entity_file("llm.entity")

@classproperty
def runtime_requirements(self):
Expand All @@ -76,35 +77,36 @@ def chat_timeout_seconds(self):
def fallback_enabled(self):
return self.settings.get("fallback_enabled", False)

# TODO: Move to __init__ after ovos-workshop stable release
def initialize(self):
self.register_entity_file("llm.entity")
# TODO: Resolve Padatious entity file handling bug
if self.fallback_enabled:
self.register_fallback(self.fallback_llm, 85)

@fallback_handler(85)
def fallback_llm(self, message):
if not self.fallback_enabled:
LOG.info("LLM Fallback Disabled")
return False
utterance = message.data['utterance']
LOG.info(f"Getting LLM response to: {utterance}")
user = get_message_user(message) or self._default_user
answer = self._get_llm_response(utterance, user, self._default_llm)
if not answer:
LOG.info(f"No fallback response")
return False
self.speak(answer)

def _threaded_get_response(utt, usr):
answer = self._get_llm_response(utt, usr, self._default_llm)
if not answer:
LOG.info(f"No fallback response")
return
self.speak(answer)

# TODO: Speak filler?
Thread(target=_threaded_get_response, args=(utterance, user), daemon=True).start()
return True

@intent_handler("enable_fallback.intent")
def handle_enable_fallback(self, message):
if not self.fallback_enabled:
self.settings['fallback_enabled'] = True
self.register_fallback(self.fallback_llm, 85)
self.speak_dialog("fallback_enabled")

@intent_handler("disable_fallback.intent")
def handle_disable_fallback(self, message):
if self.fallback_enabled:
self.settings['fallback_enabled'] = False
self.remove_fallback(self.fallback_llm)
self.speak_dialog("fallback_disabled")

@intent_handler("ask_llm.intent")
Expand All @@ -127,8 +129,7 @@ def handle_chat_with_llm(self, message):
llm = self._get_requested_llm(message)
timeout_duration = nice_duration(self.chat_timeout_seconds)
self.speak_dialog("start_chat", {"llm": llm.value,
"timeout": timeout_duration},
private=True)
"timeout": timeout_duration})
self._reset_expiration(user, llm)

@intent_handler("email_chat_history.intent")
Expand All @@ -138,15 +139,15 @@ def handle_email_chat_history(self, message):
email_addr = user_prefs['email']
if username not in self.chat_history:
LOG.debug(f"No history for {username}")
self.speak_dialog("no_chat_history", private=True)
self.speak_dialog("no_chat_history")
return
if not email_addr:
LOG.debug("No email address")
# TODO: Capture Email address
self.speak_dialog("no_email_address", private=True)
self.speak_dialog("no_email_address")
return
self.speak_dialog("sending_chat_history",
{"email": email_addr}, private=True)
{"email": email_addr})
self._send_email(username, email_addr)

def _send_email(self, username: str, email: str):
Expand All @@ -155,8 +156,7 @@ def _send_email(self, username: str, email: str):
for entry in history:
formatted = entry[1].replace('\n\n', '\n').replace('\n', '\n\t...')
email_text += f"[{entry[0]}] {formatted}\n"
NeonSkill.send_email(self, "LLM Conversation", email_text,
email_addr=email)
self.send_email("LLM Conversation", email_text, email_addr=email)

def _stop_chatting(self, message):
user = get_message_user(message) or self._default_user
Expand All @@ -174,16 +174,15 @@ def _get_llm_response(self, query: str, user: str, llm: LLM) -> str:
:returns: Speakable response to the user's query
"""
if llm == LLM.GPT:
queue = "chat_gpt_input"
endpoint = "chatgpt"
elif llm == LLM.FASTCHAT:
queue = "fastchat_input"
endpoint = "fastchat"
else:
raise ValueError(f"Expected LLM, got: {llm}")
self.chat_history.setdefault(user, list())
mq_resp = send_mq_request("/llm", {"query": query,
"history": self.chat_history[user]},
queue)
resp = mq_resp.get("response") or ""
resp = request_backend(f"/llm/{endpoint}", {"query": query, "history": self.chat_history[user]})

resp = resp.get("response") or ""
if resp:
username = "user" if user == self._default_user else user
self.chat_history[user].append((username, query))
Expand Down Expand Up @@ -237,3 +236,38 @@ def _reset_expiration(self, user, llm):
self.cancel_scheduled_event(event_name)
self.schedule_event(self._stop_chatting, self.chat_timeout_seconds,
{'user': user}, event_name)

# TODO: copied from NeonSkill. This method should be moved to a standalone
# utility
def send_email(self, title, body, message=None, email_addr=None,
attachments=None):
"""
Send an email to the registered user's email.
Method here for backwards compatibility with Mycroft skills.
Email address priority: email_addr, user prefs from message,
fallback to DeviceApi for Mycroft method
Arguments:
title (str): Title of email
body (str): HTML body of email. This supports
simple HTML like bold and italics
email_addr (str): Optional email address to send message to
attachments (dict): Optional dict of file names to Base64 encoded files
message (Message): Optional message to get email from
"""
message = message or dig_for_message()
if not email_addr and message:
email_addr = get_user_prefs(message)["user"].get("email")

if email_addr and send_mq_request:
LOG.info("Send email via Neon Server")
request_data = {"recipient": email_addr,
"subject": title,
"body": body,
"attachments": attachments}
data = send_mq_request("/neon_emails", request_data,
"neon_emails_input")
return data.get("success")
else:
LOG.warning("Attempting to send email via Mycroft Backend")
super().send_email(title, body)
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
neon-utils~=1.4
ovos-utils~=0.0.28
ovos_workshop~=0.0.11
ovos-utils~=0.0, >=0.0.28
ovos_workshop~=0.0.11,>=0.0.16a2
neon_mq_connector~=0.7
1 change: 1 addition & 0 deletions requirements/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
neon-minerva[padatious]~=0.1,>=0.1.1a1
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def find_resource_files():
url=f'https://github.com/NeonGeckoCom/{SKILL_NAME}',
license='BSD-3-Clause',
install_requires=get_requirements("requirements.txt"),
extras_require={"test": get_requirements("requirements/test.txt")},
author='Neongecko',
author_email='developers@neon.ai',
long_description=long_description,
Expand Down
4 changes: 2 additions & 2 deletions skill.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
"python": [
"neon-utils~=1.4",
"neon_mq_connector~=0.7",
"ovos-utils~=0.0.28",
"ovos_workshop~=0.0.11"
"ovos-utils~=0.0, >=0.0.28",
"ovos_workshop~=0.0.11,>=0.0.16a2"
],
"system": {},
"skill": []
Expand Down
46 changes: 6 additions & 40 deletions test/test_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,45 +28,15 @@

import unittest

from os import mkdir
from os.path import dirname, join, exists
from mock import Mock
from ovos_utils.messagebus import FakeBus
from ovos_bus_client import Message
from lingua_franca import load_language
from mycroft.skills.skill_loader import SkillLoader
from neon_minerva.tests.skill_unit_test_base import SkillTestCase

from skill_fallback_llm import LLM


class TestSkill(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
bus = FakeBus()
bus.run_in_thread()
skill_loader = SkillLoader(bus, dirname(dirname(__file__)))
skill_loader.load()
cls.skill = skill_loader.instance

# Define a directory to use for testing
cls.test_fs = join(dirname(__file__), "skill_fs")
if not exists(cls.test_fs):
mkdir(cls.test_fs)

# Override the configuration and fs paths to use the test directory
cls.skill.settings_write_path = cls.test_fs
cls.skill.file_system.path = cls.test_fs
cls.skill._init_settings()
cls.skill.initialize()

# Override speak and speak_dialog to test passed arguments
cls.skill.speak = Mock()
cls.skill.speak_dialog = Mock()

def setUp(self):
self.skill.speak.reset_mock()
self.skill.speak_dialog.reset_mock()

class TestSkill(SkillTestCase):
def test_handle_enable_fallback(self):
self.skill.handle_enable_fallback(None)
self.skill.speak_dialog.assert_called_once_with("fallback_enabled")
Expand Down Expand Up @@ -122,8 +92,7 @@ def test_handle_chat_with_llm(self):
self.assertEqual(self.skill.chatting["test_user"][1].value,
LLM.GPT.value)
self.skill.speak_dialog.assert_called_once_with(
"start_chat", {"llm": "Chat GPT", "timeout": "five minutes"},
private=True)
"start_chat", {"llm": "Chat GPT", "timeout": "five minutes"})

def test_handle_email_chat_history(self):
real_send_email = self.skill._send_email
Expand All @@ -137,22 +106,19 @@ def test_handle_email_chat_history(self):
"user_profiles": [default_profile]})
# No Chat History
self.skill.handle_email_chat_history(test_message)
self.skill.speak_dialog.assert_called_once_with("no_chat_history",
private=True)
self.skill.speak_dialog.assert_called_once_with("no_chat_history")

# No Email Address
self.skill.chat_history['test_user'] = [("user", "hey"), ("llm", "hi")]
self.skill.handle_email_chat_history(test_message)
self.skill.speak_dialog.assert_called_with("no_email_address",
private=True)
self.skill.speak_dialog.assert_called_with("no_email_address")

# Valid Request
test_message.context['user_profiles'][0]['user']['email'] = \
"test@neon.ai"
self.skill.handle_email_chat_history(test_message)
self.skill.speak_dialog.assert_called_with("sending_chat_history",
{"email": "test@neon.ai"},
private=True)
{"email": "test@neon.ai"})
self.skill._send_email.assert_called_once_with("test_user",
"test@neon.ai")

Expand Down
2 changes: 1 addition & 1 deletion version.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

__version__ = "1.0.1"
__version__ = "1.0.2a3"

0 comments on commit 34da984

Please sign in to comment.