diff --git a/.github/workflows/license_check.yml b/.github/workflows/license_check.yml new file mode 100644 index 0000000..8757eee --- /dev/null +++ b/.github/workflows/license_check.yml @@ -0,0 +1,11 @@ +name: License Check + +on: + pull_request: + branches: [dev] + workflow_dispatch: + +jobs: + license_check: + uses: OpenVoiceOS/gh-automations/.github/workflows/license-check.yml@dev + secrets: inherit diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..aa45c3d --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,14 @@ +name: Lint + +on: + pull_request: + branches: [dev, master, main] + workflow_dispatch: + +jobs: + lint: + uses: OpenVoiceOS/gh-automations/.github/workflows/lint.yml@dev + secrets: inherit + with: + ruff: true + pre_commit: false diff --git a/ovos_solver_rivescript_plugin/__init__.py b/ovos_solver_rivescript_plugin/__init__.py index d5c8cff..e829911 100644 --- a/ovos_solver_rivescript_plugin/__init__.py +++ b/ovos_solver_rivescript_plugin/__init__.py @@ -23,6 +23,23 @@ class RivescriptBot: XDG_PATH = f"{xdg_data_home()}/rivescript" os.makedirs(XDG_PATH, exist_ok=True) + # Default bot identity reflects RiveScript itself, not the upstream demo + # personality bundled in brain/en-us/begin.rive ("Aiden" from Detroit, + # Michigan - sample-brain placeholders, not creator-reflective) and not + # the Mycroft project either. RiveScript was created by Noah + # Petherbridge and first released in 2005 (originally in Perl); see + # https://www.rivescript.com/about and https://www.rivescript.com/history + # There is no sourced hometown or birthday for Petherbridge, so location/ + # city name RiveScript's own documented origin instead of the person's: + # it grew out of Chatbot::Alpha and was first written in Perl, published + # under its own root namespace on CPAN. https://www.rivescript.com/history + DEFAULT_NAME = "RiveScript" + RIVESCRIPT_BIRTH_YEAR = 2005 + DEFAULT_LOCATION = "CPAN" + DEFAULT_CITY = "the Perl programming language" + DEFAULT_MASTER = "Noah Petherbridge" + DEFAULT_WEBSITE = "rivescript.com" + def __init__(self, lang="en-us", settings=None): self.settings = settings or {} self.lang = lang @@ -36,12 +53,10 @@ def __init__(self, lang="en-us", settings=None): def load_brain(self): # secondary personal bot info - if "birthday" not in self.settings: - self.settings["birthday"] = "May 23, 2016" if "sex" not in self.settings: self.settings["sex"] = "undefined" if "master" not in self.settings: - self.settings["master"] = "skynet" + self.settings["master"] = self.DEFAULT_MASTER if "eye_color" not in self.settings: self.settings["eye_color"] = "blue" if "hair" not in self.settings: @@ -65,16 +80,19 @@ def load_brain(self): if "job" not in self.settings: self.settings["job"] = "Personal Assistant" if "website" not in self.settings: - self.settings["website"] = "openvoiceos.com" + self.settings["website"] = self.DEFAULT_WEBSITE if "pet" not in self.settings: self.settings["pet"] = "bugs" if "interests" not in self.settings: self.settings["interests"] = "I am interested in all kinds of " \ "things. We can talk about anything." + if "location" not in self.settings: + self.settings["location"] = self.DEFAULT_LOCATION + if "city" not in self.settings: + self.settings["city"] = self.DEFAULT_CITY self.rs.load_directory(self.brain_path) self.rs.sort_replies() - self.rs.set_variable("birthday", self.settings["birthday"]) self.rs.set_variable("sex", self.settings["sex"]) self.rs.set_variable("eyes", self.settings["eye_color"]) self.rs.set_variable("hair", self.settings["hair"]) @@ -91,14 +109,18 @@ def load_brain(self): self.rs.set_variable("website", self.settings["website"]) self.rs.set_variable("master", self.settings["master"]) self.rs.set_variable("interests", self.settings["interests"]) - self.rs.set_variable("name", self.settings.get("name", "mycroft")) - - self.rs.set_variable("age", str(date.today().year - 2016)) - # TODO - location from mycroft.conf - # self.rs.set_variable("location", - # self.location["city"]["state"]["country"][ - # "name"]) - # self.rs.set_variable("city", self.location_pretty) + self.rs.set_variable("name", self.settings.get("name", self.DEFAULT_NAME)) + self.rs.set_variable("location", self.settings["location"]) + self.rs.set_variable("city", self.settings["city"]) + + try: + birth_year = int(self.settings.get("birth_year", self.RIVESCRIPT_BIRTH_YEAR)) + except (TypeError, ValueError) as e: + LOG.warning(f"Invalid birth_year in config ({e}); " + f"falling back to {self.RIVESCRIPT_BIRTH_YEAR}") + birth_year = self.RIVESCRIPT_BIRTH_YEAR + age = self.settings.get("age", str(date.today().year - birth_year)) + self.rs.set_variable("age", str(age)) def ask_brain(self, utterance): try: diff --git a/test/test_bot_identity.py b/test/test_bot_identity.py new file mode 100644 index 0000000..3fee9f4 --- /dev/null +++ b/test/test_bot_identity.py @@ -0,0 +1,101 @@ +"""Regression tests: bot identity must reflect the RiveScript lineage, not +Mycroft or the upstream demo persona, and must be configurable. + +This plugin is not Mycroft and OVOS does not carry Mycroft attribution, so +the RiveScript identity variables must never default to Mycroft's identity, +and must never default to the upstream RiveScript demo personality bundled +in brain/en-us/begin.rive either ("Aiden" from Detroit, Michigan). Every +identity variable must be overridable via config. +""" +import unittest +from datetime import date + +from ovos_plugin_manager.templates.agents import AgentMessage, MessageRole + +from ovos_solver_rivescript_plugin import RivescriptBot, RivescriptChatEngine + + +class TestBotIdentity(unittest.TestCase): + def test_default_name_is_not_mycroft(self): + bot = RivescriptBot() + bot.load_brain() + name = bot.rs.get_variable("name") + self.assertNotEqual(name.lower(), "mycroft") + self.assertEqual(name, RivescriptBot.DEFAULT_NAME) + + def test_default_age_derives_from_rivescript_birth_year_not_mycroft(self): + bot = RivescriptBot() + bot.load_brain() + expected = str(date.today().year - RivescriptBot.RIVESCRIPT_BIRTH_YEAR) + self.assertEqual(bot.rs.get_variable("age"), expected) + + def test_default_master_is_not_mycroft_or_skynet(self): + bot = RivescriptBot() + bot.load_brain() + master = bot.rs.get_variable("master") + self.assertNotEqual(master.lower(), "mycroft") + self.assertNotEqual(master.lower(), "skynet") + self.assertEqual(master, RivescriptBot.DEFAULT_MASTER) + + def test_configured_name_reaches_the_answer(self): + engine = RivescriptChatEngine({"lang": "en-us", "name": "Zorb"}) + self.assertEqual(engine.brain.rs.get_variable("name"), "Zorb") + reply = engine.continue_chat( + [AgentMessage(role=MessageRole.USER, content="what is your name")] + ) + self.assertIn("zorb", reply.content.lower()) + + def test_default_location_and_city_are_not_the_upstream_demo(self): + # brain/en-us/begin.rive hardcodes "! var location = Michigan" and + # "! var city = Detroit" (the upstream RiveScript demo persona, + # "Aiden"). Both must be overridden, not left to leak through. + bot = RivescriptBot() + bot.load_brain() + self.assertNotEqual(bot.rs.get_variable("location"), "Michigan") + self.assertNotEqual(bot.rs.get_variable("city"), "Detroit") + self.assertEqual(bot.rs.get_variable("location"), RivescriptBot.DEFAULT_LOCATION) + self.assertEqual(bot.rs.get_variable("city"), RivescriptBot.DEFAULT_CITY) + + def test_default_location_and_city_name_the_language_not_the_cloud(self): + # location/city name RiveScript's own documented origin (Perl/CPAN, + # per https://www.rivescript.com/history), a fact about the + # language, not a bland placeholder and not a guess about + # Petherbridge's personal whereabouts. + bot = RivescriptBot() + bot.load_brain() + self.assertEqual(bot.rs.get_variable("location"), "CPAN") + self.assertEqual(bot.rs.get_variable("city"), "the Perl programming language") + + def test_configured_location_reaches_the_answer(self): + engine = RivescriptChatEngine({"lang": "en-us", "location": "Lisbon"}) + reply = engine.continue_chat( + [AgentMessage(role=MessageRole.USER, content="where are you from")] + ) + self.assertIn("lisbon", reply.content.lower()) + self.assertNotIn("michigan", reply.content.lower()) + + def test_configured_city_reaches_the_answer(self): + engine = RivescriptChatEngine({"lang": "en-us", "city": "Lisbon"}) + reply = engine.continue_chat( + [AgentMessage(role=MessageRole.USER, content="what city are you from")] + ) + self.assertIn("lisbon", reply.content.lower()) + self.assertNotIn("detroit", reply.content.lower()) + + def test_no_dead_birthday_constant(self): + # DEFAULT_BIRTHDAY was removed: nothing in the bundled corpus reads + # , so a constant feeding it would reach no answer. + self.assertFalse(hasattr(RivescriptBot, "DEFAULT_BIRTHDAY")) + + def test_invalid_birth_year_does_not_crash_construction(self): + # A bad config value here must degrade with a warning, not raise - + # QuestionSolversService.load_plugins has no try/except around + # plugin construction, so an uncaught ValueError here takes down the + # whole Persona, not just this handler. + engine = RivescriptChatEngine({"lang": "en-us", "birth_year": "not-a-year"}) + expected = str(date.today().year - RivescriptBot.RIVESCRIPT_BIRTH_YEAR) + self.assertEqual(engine.brain.rs.get_variable("age"), expected) + + +if __name__ == "__main__": + unittest.main()