From cf7ae70192e9a9c9a1f4f9a0039626509db46e2a Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Fri, 14 Aug 2026 01:40:36 +0100 Subject: [PATCH 1/2] feat: migrate to pyproject.toml and register as opm.agents.chat Move packaging from setup.py to pyproject.toml, matching the sibling ovos-solver-plugin-aiml layout, and register the plugin under the modern opm.agents.chat entry point group by porting RiveScript to ChatEngine. The legacy neon.plugin.solver entry point is kept pointing at the original RivescriptSolver class, so nothing that still looks there breaks. Co-Authored-By: Claude Opus 5 --- .github/workflows/build-tests.yml | 14 +++++ .github/workflows/publish_stable.yml | 77 +++++------------------ ovos_solver_rivescript_plugin/__init__.py | 54 +++++++++++++++- ovos_solver_rivescript_plugin/version.py | 8 ++- pyproject.toml | 46 ++++++++++++++ requirements.txt | 2 - setup.py | 61 ------------------ test/test_plugin.py | 62 ++++++++++++++++++ 8 files changed, 194 insertions(+), 130 deletions(-) create mode 100644 .github/workflows/build-tests.yml create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100755 setup.py create mode 100644 test/test_plugin.py 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/publish_stable.yml b/.github/workflows/publish_stable.yml index 32010b4..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.14" - - 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/ovos_solver_rivescript_plugin/__init__.py b/ovos_solver_rivescript_plugin/__init__.py index 330b5eb..d5c8cff 100644 --- a/ovos_solver_rivescript_plugin/__init__.py +++ b/ovos_solver_rivescript_plugin/__init__.py @@ -1,13 +1,23 @@ 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" @@ -124,7 +134,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 643f2af..bd238c8 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_BUILD = 2 -VERSION_ALPHA = 5 +VERSION_MINOR = 1 +VERSION_BUILD = 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/requirements.txt b/requirements.txt deleted file mode 100644 index 96e48ec..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -rivescript -ovos-plugin-manager>=0.0.26,<3.0.0 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_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() From f43448cfea8bdbf9f194e1b371c4ac6be6984457 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Fri, 14 Aug 2026 01:42:13 +0100 Subject: [PATCH 2/2] feat: add Dockerfile and ghcr publish workflow Ship a runnable image instead of just a pip-installable plugin, so RiveScript can be tried as an OpenAI-compatible chat endpoint with one docker run. The image runs ovos-persona-server with a bundled persona whose solver list names this plugin, following the pattern already used by ovos-plugin-linguonnx for its own server image. ovos-persona-server[mcp] is pinned to >=0.17.0a1 and --mcp is passed explicitly, matching that release's change where the extra alone stopped mounting the MCP endpoint. Co-Authored-By: Claude Opus 5 --- .dockerignore | 9 +++++++ .github/workflows/docker.yml | 47 ++++++++++++++++++++++++++++++++++++ Dockerfile | 35 +++++++++++++++++++++++++++ persona/rivescript_bot.json | 6 +++++ 4 files changed, 97 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker.yml create mode 100644 Dockerfile create mode 100644 persona/rivescript_bot.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..62a884d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.github +test +**/__pycache__ +*.py[cod] +.pytest_cache +*.egg-info +.venv +CHANGELOG.md diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..d37a1f2 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,47 @@ +name: docker + +on: + push: + branches: [master, dev] + tags: ["*.*.*", "V*", "v*"] + pull_request: + paths: + - Dockerfile + - .dockerignore + - pyproject.toml + - persona/** + - .github/workflows/docker.yml + workflow_dispatch: + +jobs: + docker: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v7 + - uses: docker/setup-buildx-action@v4 + - id: meta + uses: docker/metadata-action@v6 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} + type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }} + type=ref,event=tag + type=sha,format=short + - uses: docker/login-action@v4 + if: github.event_name != 'pull_request' + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/build-push-action@v7 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..af61b23 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# ovos-solver-plugin-rivescript served via ovos-persona-server. +# +# opm.agents.chat and neon.plugin.solver (legacy) do nothing on their own -- +# they are plugin entry points, not a server. This image runs +# ovos-persona-server with a single persona ("rivescript-bot") that is +# configured to use this plugin as its solver, so the image is a ready +# OpenAI-compatible chat endpoint over RiveScript. +FROM python:3.12-slim + +RUN useradd -m -u 1000 ovos + +WORKDIR /app +COPY . /app + +# ovos-persona-server[mcp]>=0.17.0a1: from that release the [mcp] extra alone +# no longer mounts the MCP endpoint, --mcp must be passed explicitly (see +# ENTRYPOINT below). The plugin itself is installed from the local checkout, +# so its version always matches the image. +RUN pip install --no-cache-dir \ + "ovos-persona-server[mcp]>=0.17.0a1" \ + && pip install --no-cache-dir . + +USER ovos +ENV HOME=/home/ovos + +EXPOSE 8337 + +# --persona points at the bundled rivescript-bot persona, whose "solvers" +# list names this plugin's entry point ("ovos-solver-rivescript-plugin"). +# The same name is registered under both opm.agents.chat and the legacy +# neon.plugin.solver group; ovos-persona-server prefers the ChatEngine. +ENTRYPOINT ["ovos-persona-server", \ + "--persona", "/app/persona/rivescript_bot.json", \ + "--mcp", \ + "--port", "8337", "--host", "0.0.0.0"] diff --git a/persona/rivescript_bot.json b/persona/rivescript_bot.json new file mode 100644 index 0000000..a363ff1 --- /dev/null +++ b/persona/rivescript_bot.json @@ -0,0 +1,6 @@ +{ + "name": "rivescript-bot", + "solvers": [ + "ovos-solver-rivescript-plugin" + ] +}