Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/license_check.yml
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -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
48 changes: 35 additions & 13 deletions ovos_solver_rivescript_plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
from datetime import date
from os.path import dirname, isdir
from typing import List, Optional

Check failure on line 4 in ovos_solver_rivescript_plugin/__init__.py

View workflow job for this annotation

GitHub Actions / lint / lint

ruff (UP035)

ovos_solver_rivescript_plugin/__init__.py:4:1: UP035 `typing.List` is deprecated, use `list` instead

from ovos_plugin_manager.templates.solvers import QuestionSolver
from ovos_utils.log import LOG
Expand All @@ -9,7 +9,7 @@
from rivescript import RiveScript

try:
from ovos_plugin_manager.templates.agents import ChatEngine, AgentMessage, MessageRole

Check failure on line 12 in ovos_solver_rivescript_plugin/__init__.py

View workflow job for this annotation

GitHub Actions / lint / lint

ruff (I001)

ovos_solver_rivescript_plugin/__init__.py:12:5: I001 Import block is un-sorted or un-formatted help: Organize imports
except ImportError:
# ovos-plugin-manager < 2.2.3a1 does not ship the agents module yet.
# The legacy QuestionSolver below still works without it; only the
Expand All @@ -23,6 +23,23 @@
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
Expand All @@ -36,12 +53,10 @@
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:
Expand All @@ -65,16 +80,19 @@
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"])
Expand All @@ -91,19 +109,23 @@
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))

Check failure on line 122 in ovos_solver_rivescript_plugin/__init__.py

View workflow job for this annotation

GitHub Actions / lint / lint

ruff (DTZ011)

ovos_solver_rivescript_plugin/__init__.py:122:44: DTZ011 `datetime.date.today()` used help: Use `datetime.datetime.now(tz=...).date()` instead
self.rs.set_variable("age", str(age))

def ask_brain(self, utterance):
try:
return self.rs.reply("human", utterance)
except Exception as e:

Check failure on line 128 in ovos_solver_rivescript_plugin/__init__.py

View workflow job for this annotation

GitHub Actions / lint / lint

ruff (BLE001)

ovos_solver_rivescript_plugin/__init__.py:128:16: BLE001 Do not catch blind exception: `Exception`
LOG.error(e)


Expand All @@ -118,8 +140,8 @@
self.brain.load_brain()

def get_spoken_answer(self, query: str,
lang: Optional[str] = None,

Check failure on line 143 in ovos_solver_rivescript_plugin/__init__.py

View workflow job for this annotation

GitHub Actions / lint / lint

ruff (FA100)

ovos_solver_rivescript_plugin/__init__.py:143:33: FA100 Add `from __future__ import annotations` to simplify `typing.Optional` help: Add `from __future__ import annotations`
units: Optional[str] = None) -> Optional[str]:

Check failure on line 144 in ovos_solver_rivescript_plugin/__init__.py

View workflow job for this annotation

GitHub Actions / lint / lint

ruff (FA100)

ovos_solver_rivescript_plugin/__init__.py:144:59: FA100 Add `from __future__ import annotations` to simplify `typing.Optional` help: Add `from __future__ import annotations`

Check failure on line 144 in ovos_solver_rivescript_plugin/__init__.py

View workflow job for this annotation

GitHub Actions / lint / lint

ruff (FA100)

ovos_solver_rivescript_plugin/__init__.py:144:34: FA100 Add `from __future__ import annotations` to simplify `typing.Optional` help: Add `from __future__ import annotations`
"""
Obtain the spoken answer for a given query.

Expand Down Expand Up @@ -151,10 +173,10 @@
self.brain = RivescriptBot(lang, self.config)
self.brain.load_brain()

def continue_chat(self, messages: List["AgentMessage"],

Check failure on line 176 in ovos_solver_rivescript_plugin/__init__.py

View workflow job for this annotation

GitHub Actions / lint / lint

ruff (UP006)

ovos_solver_rivescript_plugin/__init__.py:176:39: UP006 Use `list` instead of `List` for type annotation help: Replace with `list`
session_id: str = "default",
lang: Optional[str] = None,

Check failure on line 178 in ovos_solver_rivescript_plugin/__init__.py

View workflow job for this annotation

GitHub Actions / lint / lint

ruff (FA100)

ovos_solver_rivescript_plugin/__init__.py:178:29: FA100 Add `from __future__ import annotations` to simplify `typing.Optional` help: Add `from __future__ import annotations`
units: Optional[str] = None,

Check failure on line 179 in ovos_solver_rivescript_plugin/__init__.py

View workflow job for this annotation

GitHub Actions / lint / lint

ruff (FA100)

ovos_solver_rivescript_plugin/__init__.py:179:30: FA100 Add `from __future__ import annotations` to simplify `typing.Optional` help: Add `from __future__ import annotations`
tools=None) -> "AgentMessage":
"""
Answer the latest user message via the RiveScript brain.
Expand Down
101 changes: 101 additions & 0 deletions test/test_bot_identity.py
Original file line number Diff line number Diff line change
@@ -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
# <bot birthday>, 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()
Loading