diff --git a/.github/workflows/build-tests.yml b/.github/workflows/build-tests.yml
new file mode 100644
index 0000000..95c6f69
--- /dev/null
+++ b/.github/workflows/build-tests.yml
@@ -0,0 +1,14 @@
+name: Build Tests
+
+on:
+ pull_request:
+ branches: [dev, master]
+ workflow_dispatch:
+
+jobs:
+ build:
+ uses: OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev
+ with:
+ python_versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]'
+ install_extras: 'test'
+ test_path: 'test'
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/.github/workflows/publish_stable.yml b/.github/workflows/publish_stable.yml
index 5120025..5e43940 100644
--- a/.github/workflows/publish_stable.yml
+++ b/.github/workflows/publish_stable.yml
@@ -1,72 +1,23 @@
-name: Stable Release
+name: Publish Stable Release
+
on:
+ workflow_dispatch:
push:
branches: [master]
- workflow_dispatch:
+
+permissions:
+ contents: write
jobs:
publish_stable:
- uses: TigreGotico/gh-automations/.github/workflows/publish-stable.yml@master
- secrets: inherit
+ if: github.actor != 'github-actions[bot]'
+ uses: OpenVoiceOS/gh-automations/.github/workflows/publish-stable.yml@dev
+ secrets:
+ PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
+ MATRIX_TOKEN: ${{ secrets.MATRIX_TOKEN }}
with:
- branch: 'master'
version_file: 'ovos_solver_rivescript_plugin/version.py'
- setup_py: 'setup.py'
+ publish_pypi: true
publish_release: true
-
- publish_pypi:
- needs: publish_stable
- if: success() # Ensure this job only runs if the previous job succeeds
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- with:
- ref: dev
- fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
- - name: Setup Python
- uses: actions/setup-python@v1
- with:
- python-version: "3.11"
- - name: Install Build Tools
- run: |
- python -m pip install build wheel
- - name: version
- run: echo "::set-output name=version::$(python setup.py --version)"
- id: version
- - name: Create Release
- id: create_release
- uses: actions/create-release@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
- with:
- tag_name: V${{ steps.version.outputs.version }}
- release_name: Release ${{ steps.version.outputs.version }}
- body: |
- Changes in this Release
- ${{ steps.changelog.outputs.changelog }}
- draft: false
- prerelease: true
- commitish: dev
- - name: Build Distribution Packages
- run: |
- python setup.py sdist bdist_wheel
- - name: Publish to Test PyPI
- uses: pypa/gh-action-pypi-publish@master
- with:
- password: ${{secrets.PYPI_TOKEN}}
-
-
- sync_dev:
- needs: publish_stable
- if: success() # Ensure this job only runs if the previous job succeeds
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- with:
- fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
- ref: master
- - name: Push master -> dev
- uses: ad-m/github-push-action@master
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- branch: dev
\ No newline at end of file
+ sync_dev: true
+ notify_matrix: true
diff --git a/.github/workflows/release_workflow.yml b/.github/workflows/release_workflow.yml
index 9974fae..a27b96b 100644
--- a/.github/workflows/release_workflow.yml
+++ b/.github/workflows/release_workflow.yml
@@ -1,108 +1,22 @@
name: Release Alpha and Propose Stable
on:
+ workflow_dispatch:
pull_request:
types: [closed]
branches: [dev]
jobs:
publish_alpha:
- if: github.event.pull_request.merged == true
- uses: TigreGotico/gh-automations/.github/workflows/publish-alpha.yml@master
+ if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
+ uses: OpenVoiceOS/gh-automations/.github/workflows/publish-alpha.yml@dev
secrets: inherit
with:
branch: 'dev'
version_file: 'ovos_solver_rivescript_plugin/version.py'
- setup_py: 'setup.py'
update_changelog: true
publish_prerelease: true
+ propose_release: true
changelog_max_issues: 100
-
- notify:
- if: github.event.pull_request.merged == true
- needs: publish_alpha
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - name: Send message to Matrix bots channel
- id: matrix-chat-message
- uses: fadenb/matrix-chat-message@v0.0.6
- with:
- homeserver: 'matrix.org'
- token: ${{ secrets.MATRIX_TOKEN }}
- channel: '!WjxEKjjINpyBRPFgxl:krbel.duckdns.org'
- message: |
- new ${{ github.event.repository.name }} PR merged! https://github.com/${{ github.repository }}/pull/${{ github.event.number }}
-
- publish_pypi:
- needs: publish_alpha
- if: success() # Ensure this job only runs if the previous job succeeds
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- with:
- ref: dev
- fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
- - name: Setup Python
- uses: actions/setup-python@v1
- with:
- python-version: "3.11"
- - name: Install Build Tools
- run: |
- python -m pip install build wheel
- - name: version
- run: echo "::set-output name=version::$(python setup.py --version)"
- id: version
- - name: Build Distribution Packages
- run: |
- python setup.py sdist bdist_wheel
- - name: Publish to Test PyPI
- uses: pypa/gh-action-pypi-publish@master
- with:
- password: ${{secrets.PYPI_TOKEN}}
-
-
- propose_release:
- needs: publish_alpha
- if: success() # Ensure this job only runs if the previous job succeeds
- runs-on: ubuntu-latest
- steps:
- - name: Checkout dev branch
- uses: actions/checkout@v3
- with:
- ref: dev
-
- - name: Setup Python
- uses: actions/setup-python@v2
- with:
- python-version: '3.10'
-
- - name: Get version from setup.py
- id: get_version
- run: |
- VERSION=$(python setup.py --version)
- echo "VERSION=$VERSION" >> $GITHUB_ENV
-
- - name: Create and push new branch
- run: |
- git checkout -b release-${{ env.VERSION }}
- git push origin release-${{ env.VERSION }}
-
- - name: Open Pull Request from dev to master
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- # Variables
- BRANCH_NAME="release-${{ env.VERSION }}"
- BASE_BRANCH="master"
- HEAD_BRANCH="release-${{ env.VERSION }}"
- PR_TITLE="Release ${{ env.VERSION }}"
- PR_BODY="Human review requested!"
-
- # Create a PR using GitHub API
- curl -X POST \
- -H "Accept: application/vnd.github+json" \
- -H "Authorization: token $GITHUB_TOKEN" \
- -d "{\"title\":\"$PR_TITLE\",\"body\":\"$PR_BODY\",\"head\":\"$HEAD_BRANCH\",\"base\":\"$BASE_BRANCH\"}" \
- https://api.github.com/repos/${{ github.repository }}/pulls
-
+ publish_pypi: true
+ notify_matrix: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ba439cd..5e43af8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,12 +1,64 @@
# Changelog
-## [0.0.1a1](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/tree/0.0.1a1) (2025-06-08)
+## [0.1.1a1](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/tree/0.1.1a1) (2026-08-15)
-[Full Changelog](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/compare/0.0.0...0.0.1a1)
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/compare/0.1.0a2...0.1.1a1)
**Merged pull requests:**
-- fix: compatibility with ovos-plugin-manager 1.X.X [\#1](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/pull/1) ([JarbasAl](https://github.com/JarbasAl))
+- fix: bot identity reflects RiveScript's own origins, not Mycroft or the demo brain [\#22](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/pull/22) ([JarbasAl](https://github.com/JarbasAl))
+
+## [0.1.0a2](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/tree/0.1.0a2) (2026-08-14)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/compare/0.0.2a5...0.1.0a2)
+
+**Merged pull requests:**
+
+- Migrate to pyproject.toml and register as ChatEngine \(opm.agents.chat\) [\#18](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/pull/18) ([JarbasAl](https://github.com/JarbasAl))
+
+## [0.0.2a5](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/tree/0.0.2a5) (2026-08-02)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/compare/0.0.2a4...0.0.2a5)
+
+**Merged pull requests:**
+
+- ci: migrate release workflow to shared OpenVoiceOS automations [\#16](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/pull/16) ([JarbasAl](https://github.com/JarbasAl))
+
+## [0.0.2a4](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/tree/0.0.2a4) (2026-08-01)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/compare/0.0.2a3...0.0.2a4)
+
+**Merged pull requests:**
+
+- docs: rewrite README in Simplified Technical English [\#15](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/pull/15) ([JarbasAl](https://github.com/JarbasAl))
+
+## [0.0.2a3](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/tree/0.0.2a3) (2025-12-19)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/compare/0.0.2a2...0.0.2a3)
+
+**Merged pull requests:**
+
+- chore\(deps\): update dependency python to 3.14 [\#6](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/pull/6) ([renovate[bot]](https://github.com/apps/renovate))
+
+## [0.0.2a2](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/tree/0.0.2a2) (2025-12-18)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/compare/0.0.2a1...0.0.2a2)
+
+**Merged pull requests:**
+
+- chore: Configure Renovate [\#5](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/pull/5) ([renovate[bot]](https://github.com/apps/renovate))
+
+## [0.0.2a1](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/tree/0.0.2a1) (2025-11-22)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/compare/V0.0.1...0.0.2a1)
+
+**Merged pull requests:**
+
+- Update requirements.txt [\#3](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/pull/3) ([JarbasAl](https://github.com/JarbasAl))
+
+## [V0.0.1](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/tree/V0.0.1) (2025-06-08)
+
+[Full Changelog](https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript/compare/0.0.1...V0.0.1)
diff --git a/README.md b/README.md
index 6f18a51..efb8d48 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,16 @@
#
Rivescript Chatbot
-
-Give Mycroft some sass with Rivescript!
-Leverages the [Alice chatbot](https://www.chatbots.org/chatbot/a.l.i.c.e/) to create some fun interactions. Phrases not explicitly handled by other skills will be run by the chatbot, so nearly every interaction will have _some_ response. But be warned, Mycroft might become a bit obnoxious...
+This plugin is a question solver that answers with a [Rivescript](https://www.rivescript.com/) chatbot. It uses the [Alice chatbot](https://www.chatbots.org/chatbot/a.l.i.c.e/) brain to answer phrases that no other skill handles, so almost every input gets some response. Answers can be casual and a bit sassy.
+
+## Examples
-## Examples
* "Do you like ice cream"
* "Do you like dogs"
* "I have a jump rope"
-
## Usage
-Spoken answers api with a Rivescript backend
+The plugin exposes a spoken-answers API with a Rivescript backend.
```python
from ovos_solver_rivescript_plugin import RivescriptSolver
@@ -26,3 +24,8 @@ sentence = d.spoken_answer("Do you like ice cream", {"lang": "pt-pt"})
print(sentence)
# O que queres mesmo saber?
```
+
+## Related projects
+
+* [OpenVoiceOS/ovos-plugin-manager](https://github.com/OpenVoiceOS/ovos-plugin-manager): loads and manages solver plugins like this one.
+* [OpenVoiceOS/ovos-persona-server](https://github.com/OpenVoiceOS/ovos-persona-server): runs solver plugins as chat personas.
diff --git a/ovos_solver_rivescript_plugin/__init__.py b/ovos_solver_rivescript_plugin/__init__.py
index 330b5eb..e829911 100644
--- a/ovos_solver_rivescript_plugin/__init__.py
+++ b/ovos_solver_rivescript_plugin/__init__.py
@@ -1,18 +1,45 @@
import os
from datetime import date
from os.path import dirname, isdir
-from typing import Optional
+from typing import List, Optional
from ovos_plugin_manager.templates.solvers import QuestionSolver
from ovos_utils.log import LOG
from ovos_utils.xdg_utils import xdg_data_home
from rivescript import RiveScript
+try:
+ from ovos_plugin_manager.templates.agents import ChatEngine, AgentMessage, MessageRole
+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
+ # ChatEngine registration is unavailable on such an old install.
+ ChatEngine = object
+ AgentMessage = None
+ MessageRole = None
+
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
@@ -26,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:
@@ -55,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"])
@@ -81,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("name", self.settings.get("name", self.DEFAULT_NAME))
+ self.rs.set_variable("location", self.settings["location"])
+ self.rs.set_variable("city", self.settings["city"])
- 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)
+ 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:
@@ -124,7 +156,49 @@ def get_spoken_answer(self, query: str,
return self.brain.ask_brain(query)
+class RivescriptChatEngine(ChatEngine):
+ """RiveScript chatbot exposed as a modern ChatEngine agent plugin.
+
+ RiveScript is a pattern-matching chatbot: it has no notion of tool
+ calling, so ``tools`` is accepted (callers pass it by keyword) and
+ ignored, and ``supports_tools`` stays at the base default of False.
+ """
+
+ def __init__(self, config=None):
+ config = config or {"lang": "en-us"}
+ lang = config.get("lang") or "en-us"
+ if lang != "en-us" and lang not in os.listdir(RivescriptBot.XDG_PATH):
+ config["lang"] = lang = "en-us"
+ super().__init__(config)
+ self.brain = RivescriptBot(lang, self.config)
+ self.brain.load_brain()
+
+ def continue_chat(self, messages: List["AgentMessage"],
+ session_id: str = "default",
+ lang: Optional[str] = None,
+ units: Optional[str] = None,
+ tools=None) -> "AgentMessage":
+ """
+ Answer the latest user message via the RiveScript brain.
+
+ RiveScript itself has no concept of chat history beyond the single
+ reply it is asked for, so only the most recent user message is used;
+ earlier turns in ``messages`` are ignored, same as upstream RiveScript
+ usage elsewhere in this plugin.
+ """
+ query = next((m.content for m in reversed(messages)
+ if m.role == MessageRole.USER), "")
+ if not query:
+ return AgentMessage(role=MessageRole.ASSISTANT, content="")
+ answer = self.brain.ask_brain(query) or ""
+ return AgentMessage(role=MessageRole.ASSISTANT, content=answer)
+
+
if __name__ == "__main__":
bot = RivescriptSolver()
print(bot.get_spoken_answer("hello!"))
print(bot.spoken_answer("Qual é a tua comida favorita?", lang="pt-pt"))
+
+ chat = RivescriptChatEngine()
+ reply = chat.continue_chat([AgentMessage(role=MessageRole.USER, content="hello!")])
+ print(reply.content)
diff --git a/ovos_solver_rivescript_plugin/version.py b/ovos_solver_rivescript_plugin/version.py
index 76c4342..cc79e14 100644
--- a/ovos_solver_rivescript_plugin/version.py
+++ b/ovos_solver_rivescript_plugin/version.py
@@ -1,6 +1,8 @@
# START_VERSION_BLOCK
VERSION_MAJOR = 0
-VERSION_MINOR = 0
+VERSION_MINOR = 1
VERSION_BUILD = 1
-VERSION_ALPHA = 0
+VERSION_ALPHA = 1
# END_VERSION_BLOCK
+
+__version__ = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}" + (f"a{VERSION_ALPHA}" if VERSION_ALPHA else "")
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..96ea131
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,46 @@
+[build-system]
+requires = ["setuptools>=61.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "ovos-solver-rivescript-plugin"
+dynamic = ["readme", "version"]
+description = "A question solver plugin for ovos/neon/mycroft"
+authors = [{ name = "JarbasAi", email = "jarbasai@mailfence.com" }]
+license = { text = "MIT" }
+requires-python = ">=3.9"
+keywords = ["mycroft", "plugin", "utterance", "fallback", "query", "rivescript"]
+
+dependencies = [
+ "rivescript",
+ "ovos-plugin-manager>=2.6.1a1,<3.0.0",
+]
+
+[project.urls]
+Homepage = "https://github.com/OpenVoiceOS/ovos-solver-plugin-rivescript"
+
+[project.optional-dependencies]
+test = [
+ "pytest>=7.0.0,<9",
+ "pytest-timeout>=2.0.0",
+]
+
+[project.entry-points."opm.agents.chat"]
+"ovos-solver-rivescript-plugin" = "ovos_solver_rivescript_plugin:RivescriptChatEngine"
+
+[project.entry-points."neon.plugin.solver"]
+"ovos-solver-rivescript-plugin" = "ovos_solver_rivescript_plugin:RivescriptSolver"
+
+[tool.setuptools]
+packages = ["ovos_solver_rivescript_plugin"]
+include-package-data = true
+
+[tool.setuptools.package-data]
+ovos_solver_rivescript_plugin = ["brain/**"]
+
+[tool.setuptools.dynamic]
+readme = { file = "README.md", content-type = "text/markdown" }
+version = { attr = "ovos_solver_rivescript_plugin.version.__version__" }
+
+[tool.pytest.ini_options]
+testpaths = ["test"]
diff --git a/renovate.json b/renovate.json
new file mode 100644
index 0000000..5db72dd
--- /dev/null
+++ b/renovate.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "config:recommended"
+ ]
+}
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 51cd62f..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-rivescript
-ovos-plugin-manager>=0.0.26,<2.0.0
\ No newline at end of file
diff --git a/setup.py b/setup.py
deleted file mode 100755
index 3f2d139..0000000
--- a/setup.py
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env python3
-import os
-from setuptools import setup
-
-BASEDIR = os.path.abspath(os.path.dirname(__file__))
-
-
-def required(requirements_file):
- """ Read requirements file and remove comments and empty lines. """
- with open(os.path.join(BASEDIR, requirements_file), 'r') as f:
- requirements = f.read().splitlines()
- if 'MYCROFT_LOOSE_REQUIREMENTS' in os.environ:
- print('USING LOOSE REQUIREMENTS!')
- requirements = [r.replace('==', '>=').replace('~=', '>=') for r in requirements]
- return [pkg for pkg in requirements
- if pkg.strip() and not pkg.startswith("#")]
-
-
-with open(f"{BASEDIR}/README.md", "r") as f:
- long_description = f.read()
-
-def get_version():
- """ Find the version of the package"""
- version_file = os.path.join(BASEDIR, 'ovos_solver_rivescript_plugin', 'version.py')
- major, minor, build, alpha = (None, None, None, None)
- with open(version_file) as f:
- for line in f:
- if 'VERSION_MAJOR' in line:
- major = line.split('=')[1].strip()
- elif 'VERSION_MINOR' in line:
- minor = line.split('=')[1].strip()
- elif 'VERSION_BUILD' in line:
- build = line.split('=')[1].strip()
- elif 'VERSION_ALPHA' in line:
- alpha = line.split('=')[1].strip()
-
- if ((major and minor and build and alpha) or
- '# END_VERSION_BLOCK' in line):
- break
- version = f"{major}.{minor}.{build}"
- if alpha and int(alpha) > 0:
- version += f"a{alpha}"
- return version
-
-PLUGIN_ENTRY_POINT = 'ovos-solver-rivescript-plugin=ovos_solver_rivescript_plugin:RivescriptSolver'
-setup(
- name='ovos-solver-rivescript-plugin',
- version=get_version(),
- description='A question solver plugin for ovos/neon/mycroft',
- url='https://github.com/OpenVoiceOS/ovos-solver-rivescript-plugin',
- author='jarbasai',
- author_email='jarbasai@mailfence.com',
- license='MIT',
- packages=['ovos_solver_rivescript_plugin'],
- zip_safe=True,
- keywords='mycroft plugin utterance fallback query',
- entry_points={'neon.plugin.solver': PLUGIN_ENTRY_POINT},
- install_requires=required("requirements.txt"),
- long_description=long_description,
- long_description_content_type='text/markdown'
-)
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()
diff --git a/test/test_plugin.py b/test/test_plugin.py
new file mode 100644
index 0000000..8d00c12
--- /dev/null
+++ b/test/test_plugin.py
@@ -0,0 +1,62 @@
+"""Smoke tests: the plugin loads and answers, and is discoverable under both
+the legacy question-solver entry point and the modern chat-engine entry
+point.
+"""
+import unittest
+
+from ovos_plugin_manager.templates.agents import AgentMessage, MessageRole
+from ovos_plugin_manager.utils import find_plugins
+
+from ovos_solver_rivescript_plugin import RivescriptBot, RivescriptChatEngine, RivescriptSolver
+
+
+class TestRivescriptSolver(unittest.TestCase):
+ def test_brain_answers(self):
+ # RivescriptSolver.__init__ hardcodes enable_tx=True, which makes the
+ # base QuestionSolver eagerly build a language-translation plugin at
+ # construction time even when no translation is ever performed. That
+ # is a pre-existing base-class quirk unrelated to this migration, and
+ # it means constructing RivescriptSolver requires a translate plugin
+ # to be installed. Exercise the underlying brain directly instead,
+ # which is what actually answers queries.
+ bot = RivescriptBot()
+ bot.load_brain()
+ answer = bot.ask_brain("hello")
+ self.assertIsInstance(answer, str)
+ self.assertTrue(answer)
+
+ def test_registered_under_legacy_group(self):
+ plugins = find_plugins("neon.plugin.solver")
+ self.assertIn("ovos-solver-rivescript-plugin", plugins)
+ self.assertIs(plugins["ovos-solver-rivescript-plugin"], RivescriptSolver)
+
+
+class TestRivescriptChatEngine(unittest.TestCase):
+ def test_continue_chat(self):
+ engine = RivescriptChatEngine()
+ reply = engine.continue_chat(
+ [AgentMessage(role=MessageRole.USER, content="hello")]
+ )
+ self.assertIsInstance(reply, AgentMessage)
+ self.assertEqual(reply.role, MessageRole.ASSISTANT)
+ self.assertTrue(reply.content)
+
+ def test_continue_chat_accepts_and_ignores_tools(self):
+ # ChatEngine.continue_chat callers pass tools= by keyword; a pattern
+ # matcher has no use for it but must still accept it without raising.
+ engine = RivescriptChatEngine()
+ reply = engine.continue_chat(
+ [AgentMessage(role=MessageRole.USER, content="hello")],
+ tools=[{"type": "function", "function": {"name": "noop"}}],
+ )
+ self.assertIsInstance(reply, AgentMessage)
+ self.assertFalse(engine.supports_tools)
+
+ def test_registered_under_chat_group(self):
+ plugins = find_plugins("opm.agents.chat")
+ self.assertIn("ovos-solver-rivescript-plugin", plugins)
+ self.assertIs(plugins["ovos-solver-rivescript-plugin"], RivescriptChatEngine)
+
+
+if __name__ == "__main__":
+ unittest.main()