Skip to content
Draft
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
15 changes: 15 additions & 0 deletions .github/workflows/build-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Build Tests

on:
pull_request:
branches: [dev, master, main]
workflow_dispatch:

jobs:
build:
uses: OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev
secrets: inherit
with:
python_versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]'
install_extras: 'test'
test_path: 'test/unittests'
13 changes: 13 additions & 0 deletions .github/workflows/license_check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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
with:
exclude_packages: '(?i:^ovos-solver-rivescript-plugin$)'
13 changes: 13 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Lint

on:
pull_request:
branches: [dev, master, main]
workflow_dispatch:

jobs:
lint:
uses: OpenVoiceOS/gh-automations/.github/workflows/lint.yml@dev
with:
ruff: true
pre_commit: false
14 changes: 14 additions & 0 deletions .github/workflows/opm-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: OPM Plugin Check

on:
pull_request:
branches: [dev, master, main]
workflow_dispatch:

jobs:
opm_check:
uses: OpenVoiceOS/gh-automations/.github/workflows/opm-check.yml@dev
secrets: inherit
with:
python_version: '3.11'
plugin_type: 'auto'
15 changes: 15 additions & 0 deletions .github/workflows/ovoscope.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Skill End-to-End Tests (ovoscope)

on:
pull_request:
branches: [dev, master, main]
workflow_dispatch:

jobs:
ovoscope:
uses: OpenVoiceOS/gh-automations/.github/workflows/ovoscope.yml@dev
secrets: inherit
with:
python_version: '3.11'
install_extras: 'test'
test_path: 'test/end2end/'
49 changes: 49 additions & 0 deletions .github/workflows/regenerate-brain.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Regenerates ovos_solver_rivescript_plugin/brain/<lang>/generated.rive from
# locale/<lang>/ whenever locale changes land on dev.
# Pushing to dev does NOT trigger the release workflow (release_workflow.yml fires
# only on pull_request closed → dev, not on push), so this commit loop is safe.
name: Regenerate RiveScript brain

on:
push:
branches: [dev]
workflow_dispatch:

permissions:
contents: write

jobs:
regenerate:
runs-on: ubuntu-latest
steps:
- name: Checkout dev
uses: actions/checkout@v4
with:
ref: dev
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Regenerate brain files from locale
run: |
for lang_dir in ovos_solver_rivescript_plugin/locale/*/; do
lang=$(basename "$lang_dir")
out_dir="ovos_solver_rivescript_plugin/brain/${lang}"
mkdir -p "$out_dir"
python scripts/locale_to_brain.py rivescript "$lang_dir" "${out_dir}/generated.rive"
done

- name: Commit if changed
run: |
git config user.name "JarbasAi"
git config user.email "jarbasai@mailfence.com"
if git diff --quiet -- 'ovos_solver_rivescript_plugin/brain/'; then
echo "No brain changes — nothing to commit."
exit 0
fi
git add ovos_solver_rivescript_plugin/brain/
git commit -m "chore: regenerate RiveScript brain from locale"
git push
13 changes: 13 additions & 0 deletions .github/workflows/release-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Release Preview

on:
pull_request:
branches: [dev]
workflow_dispatch:

jobs:
release_preview:
uses: OpenVoiceOS/gh-automations/.github/workflows/release-preview.yml@dev
with:
package_name: 'ovos_solver_rivescript_plugin'
version_file: 'ovos_solver_rivescript_plugin/version.py'
7 changes: 4 additions & 3 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
recursive-include LICENSE
recursive-include README.md
recursive-include requirements.txt
include LICENSE
include README.md
include requirements.txt
recursive-include ovos_solver_rivescript_plugin *
71 changes: 58 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,73 @@
# <img src='https://raw.githack.com/FortAwesome/Font-Awesome/master/svgs/solid/robot.svg' card_color='#40DBB0' width='50' height='50' style='vertical-align:bottom'/> 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...
Give OVOS some sass with RiveScript!

Leverages `.rive` brain files to create fun, persona-driven interactions. Phrases not explicitly handled by other skills will be handled by the chatbot, so nearly every interaction will have _some_ response.

## Examples

## Examples
* "Do you like ice cream"
* "Do you like dogs"
* "I have a jump rope"

* "hello"
* "what is your name"

## Usage

Spoken answers api with a Rivescript backend
ChatEngine API backed by a RiveScript brain:

```python
from ovos_solver_rivescript_plugin import RivescriptSolver
from ovos_plugin_manager.templates.agents import AgentMessage, MessageRole
from ovos_solver_rivescript_plugin import RiveScriptChatEngine

d = RivescriptSolver()
sentence = d.spoken_answer("hello")
print(sentence)
engine = RiveScriptChatEngine()
reply = engine.continue_chat([AgentMessage(role=MessageRole.USER, content="hello")])
print(reply.content)
# Hi there!
```

## Configuration

| Key | Default | Description |
|-----|---------|-------------|
| `lang` | `"en-us"` | Brain language to load. Must match a directory under `brain/` or the XDG data dir. |
| `enable_tx` | `False` | Opt-in brain-file translation (see below). |
| `translate_plugin` | `"ovos-translate-plugin-server"` | Translation plugin ID to use when `enable_tx` is on. |

### Opt-in brain-file translation (`enable_tx`)

By default the plugin only supports English (`en-us`). Setting `enable_tx: true` lets the plugin serve non-English users by translating the bundled English `.rive` brain into the requested language.

sentence = d.spoken_answer("Do you like ice cream", {"lang": "pt-pt"})
print(sentence)
# O que queres mesmo saber?
```json
{
"ovos-solver-rivescript-plugin": {
"lang": "pt-pt",
"enable_tx": true,
"translate_plugin": "ovos-translate-plugin-server"
}
}
```

**How it works:**

1. When `lang` is requested and no native `.rive` brain exists for it, the plugin translates the English brain into the target language.
2. Only human-readable text in trigger lines (`+ …`) and response lines (`- …`) is translated. RiveScript control syntax — wildcards (`*`, `_`), tags (`<star>`, `<get>`, `<set>`, `{topic=…}`, …), optional groups (`[…]`, `(…)`), label markers (`>`, `<`), definitions (`!`), and redirects (`@`, `^`) — is preserved verbatim.
3. The translated `.rive` files are cached in the XDG data directory (`~/.local/share/rivescript/<lang>-tx/`) and reused on subsequent runs without re-translating.
4. If the translator cannot be loaded or translation fails, the plugin falls back to the English brain without crashing.

**Recommended translate plugin:** `ovos-translate-plugin-server` (the default) or another remote plugin. Translation plugins may be instantiated multiple times across different personas, so local model plugins that load a large model into memory on each instantiation are costly. A remote plugin delegates inference to a server and avoids this overhead.

**Default behaviour is unchanged:** with `enable_tx` off (the default), no translator is loaded, the English brain is used, and there is no runtime cost.

## Contributing intents (OVOS locale)

Conversational content lives in `ovos_solver_rivescript_plugin/locale/` as paired
OVOS intent/dialog files — **not** directly in `.rive` files. After a PR merges
to `dev`, a CI workflow automatically regenerates the RiveScript brain.

- **[docs/locale.md](docs/locale.md)** — source-of-truth layout, the `{query}`
slot, how to add an intent, how to translate to a new language, and a full table
of supported vs. skipped RiveScript constructs.
- **[docs/converters.md](docs/converters.md)** — the two converter scripts
(`brain_to_locale.py`, `locale_to_brain.py`), usage examples, round-trip
semantics, and the ~52 % clean-conversion caveat for legacy brain files.
82 changes: 82 additions & 0 deletions docs/converters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Converter scripts

Two stdlib-only scripts (no third-party dependencies) handle round-trip
conversion between OVOS `locale/` notation and RiveScript / AIML brain files.

## `scripts/brain_to_locale.py` — brain → locale

Converts an existing `.rive` or `.aiml` brain directory into OVOS `locale/`
intent/dialog pairs. Run once to bootstrap a `locale/` tree from a legacy brain.

```
python scripts/brain_to_locale.py rivescript <rive_dir> <out_locale_dir>
python scripts/brain_to_locale.py aiml <aiml_dir> <out_locale_dir>
```

**Example — bootstrap English locale from the bundled brain:**

```bash
python scripts/brain_to_locale.py \
rivescript \
ovos_solver_rivescript_plugin/brain/en-us \
ovos_solver_rivescript_plugin/locale/en-us
# rivescript: converted 134/255 entries (52%), skipped 121 (unmappable constructs) -> …/locale/en-us
```

The ~52 % conversion rate reflects the fact that roughly half the triggers in the
original brain use constructs (`%`, `@`, `{topic}`, `<call>`, arrays, multi-star)
that have no direct OVOS equivalent. Those 121 entries are **not deleted** — they
remain in the legacy `.rive` files and continue to be loaded by the engine.

## `scripts/locale_to_brain.py` — locale → brain

The reverse direction. This is what the CI workflow calls after every merge to
`dev`.

```
python scripts/locale_to_brain.py rivescript <locale_dir> <out.rive>
python scripts/locale_to_brain.py aiml <locale_dir> <out.aiml>
```

**Example — regenerate brain from locale (all pairs → one file):**

```bash
python scripts/locale_to_brain.py \
rivescript \
ovos_solver_rivescript_plugin/locale/en-us \
ovos_solver_rivescript_plugin/brain/en-us/generated.rive
# rivescript: compiled 134 intents from …/locale/en-us -> …/brain/en-us/generated.rive
```

## Round-trip behaviour

```
locale/en-us/ →(locale_to_brain)→ brain/en-us/generated.rive
←(brain_to_locale)← brain/en-us/generated.rive
```

The round-trip is lossless for the supported subset:

- `{query}` slot ↔ `*` trigger wildcard ↔ `<star>` response tag.
- Multiple `.dialog` lines ↔ multiple `- response` lines (random set).
- Multiple `.intent` lines ↔ canonical trigger + `@` redirect lines.

A second call to `locale_to_brain.py` with no locale edits produces an identical
`.rive` file (idempotent).

## The ~52 % conversion caveat

`brain_to_locale.py` skips any trigger/response pair that involves:

- `%` previous-input constraints
- `@` redirect targets (as the *source* of a redirect)
- `{topic=…}` topic switches
- `<call>` object macros
- `(@array)` array references
- `<star2>` / `<star3>` multi-capture (more than one wildcard)

These entries stay in the original `.rive` files. The engine loads **both** the
legacy files and `generated.rive`, so nothing is lost.

New content added via `locale/` will have 100 % round-trip fidelity as long as
authors stick to the supported constructs documented in [`locale.md`](locale.md).
Loading
Loading