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
14 changes: 14 additions & 0 deletions .github/workflows/build-tests.yml
Original file line number Diff line number Diff line change
@@ -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'
77 changes: 14 additions & 63 deletions .github/workflows/publish_stable.yml
Original file line number Diff line number Diff line change
@@ -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
sync_dev: true
notify_matrix: true
54 changes: 53 additions & 1 deletion ovos_solver_rivescript_plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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)
8 changes: 5 additions & 3 deletions ovos_solver_rivescript_plugin/version.py
Original file line number Diff line number Diff line change
@@ -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 "")
46 changes: 46 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"]
2 changes: 0 additions & 2 deletions requirements.txt

This file was deleted.

61 changes: 0 additions & 61 deletions setup.py

This file was deleted.

62 changes: 62 additions & 0 deletions test/test_plugin.py
Original file line number Diff line number Diff line change
@@ -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()
Loading