Skip to content
This repository has been archived by the owner on Jan 31, 2025. It is now read-only.

feat: populate default settings, warn user if key missing #19

Merged
merged 7 commits into from
Feb 17, 2024
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
54 changes: 28 additions & 26 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,43 @@ on:
branches:
- dev
paths-ignore:
- 'version.py'
- 'requirements.txt'
- 'examples/**'
- '.github/**'
- '.gitignore'
- 'LICENSE'
- 'CHANGELOG.md'
- 'MANIFEST.in'
- 'readme.md'
- 'scripts/**'
- "version.py"
- "requirements.txt"
- "examples/**"
- ".github/**"
- ".gitignore"
- "LICENSE"
- "CHANGELOG.md"
- "MANIFEST.in"
- "readme.md"
- "scripts/**"
push:
branches:
- master
paths-ignore:
- 'version.py'
- 'requirements.txt'
- 'examples/**'
- '.github/**'
- '.gitignore'
- 'LICENSE'
- 'CHANGELOG.md'
- 'MANIFEST.in'
- 'readme.md'
- 'scripts/**'
- "version.py"
- "requirements.txt"
- "examples/**"
- ".github/**"
- ".gitignore"
- "LICENSE"
- "CHANGELOG.md"
- "MANIFEST.in"
- "readme.md"
- "scripts/**"
workflow_dispatch:

jobs:
unit_tests:
strategy:
max-parallel: 2
matrix:
python-version: [ 3.7, 3.8, 3.9, "3.10" ]
python-version: [3.8, 3.9, "3.10", "3.11"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install System Dependencies
Expand All @@ -57,17 +57,19 @@ jobs:
sudo apt install libfann-dev
- name: Install ovos dependencies
run: |
pip install ovos-plugin-manager ovos-core[skills_lgpl]>=0.0.6a21
pip install --pre ovos-plugin-manager ovos-core[skills_lgpl]>=0.0.8
- name: Install core repo
run: |
pip install .
pip install --pre -U ovos-workshop # 0.0.16a5 required for the class to load properly in unit tests
pip list
- name: Run unittests
run: |
pytest --cov=ovos-skill-template-repo --cov-report xml test/unittests
pytest --cov=. --cov-report xml test/unittests
# NOTE: additional pytest invocations should also add the --cov-append flag
# or they will overwrite previous invocations' coverage reports
# (for an example, see OVOS Skill Manager's workflow)
- name: Upload coverage
env:
CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}}
uses: codecov/codecov-action@v2
uses: codecov/codecov-action@v4
17 changes: 14 additions & 3 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
from ovos_workshop.skills.fallback import FallbackSkill


DEFAULT_SETTINGS = {
"persona": "You are a helpful voice assistant with a friendly tone and fun sense of humor. You respond in 40 words or fewer.",
"model": "gpt-3.5-turbo",
}

class ChatGPTSkill(FallbackSkill):
sessions = {}

Expand All @@ -18,14 +23,19 @@ def runtime_requirements(self):
)

def initialize(self):
self.settings.merge(DEFAULT_SETTINGS, new_only=True)
self.add_event("speak", self.handle_speak)
self.add_event("recognizer_loop:utterance", self.handle_utterance)
self.register_fallback(self.ask_chatgpt, 85)

@property
def chat(self):
"""created fresh to allow key/url rotation when settings.json is edited"""
return OpenAIPersonaSolver(config=self.settings)
try:
return OpenAIPersonaSolver(config=self.settings)
except Exception as err:
self.log.error(err)
return None

def handle_utterance(self, message):
utt = message.data.get("utterances")[0]
Expand Down Expand Up @@ -75,13 +85,14 @@ def _async_ask(self, message):
for utt in self.chat.stream_utterances(utterance):
answered = True
self.speak(utt)
except: # speak error on any network issue / no credits etc
pass
except Exception as err: # speak error on any network issue / no credits etc
self.log.error(err)
if not answered:
self.speak_dialog("gpt_error")

def ask_chatgpt(self, message):
if "key" not in self.settings:
self.log.error("ChatGPT not configured yet, please set your API key in %s", self.settings.path)
return False # ChatGPT not configured yet
utterance = message.data["utterance"]
self.speak_dialog("asking")
Expand Down
Empty file added test/unittests/__init__.py
Empty file.
76 changes: 76 additions & 0 deletions test/unittests/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import shutil
from os import environ, getenv
from os.path import dirname, join
from threading import Event
from time import sleep
from unittest import TestCase
from unittest.mock import MagicMock, Mock

import pytest
from ovos_utils.fakebus import FakeBus
from skill_ovos_fallback_chatgpt import DEFAULT_SETTINGS, ChatGPTSkill


class TestChatGPTSkill(TestCase):
# Define test directories
test_fs = join(dirname(__file__), "skill_fs")
data_dir = join(test_fs, "data")
conf_dir = join(test_fs, "config")
environ["XDG_DATA_HOME"] = data_dir
environ["XDG_CONFIG_HOME"] = conf_dir

bus = FakeBus()
bus.emitter = bus.ee
bus.connected_event = Event()
bus.connected_event.set()
bus.run_forever()
test_skill_id = 'test_skill.test'

skill = None

@classmethod
def setUpClass(cls) -> None:
# Get test skill
cls.skill = ChatGPTSkill(skill_id=cls.test_skill_id, bus=cls.bus)
# Override speak and speak_dialog to test passed arguments
cls.skill.speak = Mock()
cls.skill.speak_dialog = Mock()

def setUp(self):
self.skill.speak.reset_mock()
self.skill.speak_dialog.reset_mock()
self.skill.play_audio = Mock()
self.skill.log = MagicMock()

@classmethod
def tearDownClass(cls) -> None:
shutil.rmtree(cls.test_fs)

def test_default_no_key(self):
assert not self.skill.settings.get("key")
self.skill.ask_chatgpt("Will my test pass?")
self.skill.log.error.assert_called()
self.skill.speak_dialog.assert_not_called() # no key, we log an error before speaking ever happens
assert self.skill.settings.get("persona") == DEFAULT_SETTINGS["persona"]
assert self.skill.settings.get("model") == DEFAULT_SETTINGS["model"]

def test_default_with_key(self):
self.skill.settings["key"] = "test"
self.skill.settings.store()
assert self.skill.settings.get("key") == "test"
assert self.skill.settings.get("persona") == DEFAULT_SETTINGS["persona"]
assert self.skill.settings.get("model") == DEFAULT_SETTINGS["model"]

def test_overriding_all_settings(self):
self.skill.settings["key"] = "test"
self.skill.settings["persona"] = "I am a test persona"
self.skill.settings["model"] = "gpt-4-nitro"
self.skill.settings.store()
assert self.skill.settings.get("key") == "test"
assert self.skill.settings.get("persona") == "I am a test persona"
assert self.skill.settings.get("model") == "gpt-4-nitro"
assert self.skill.settings.get("persona") != DEFAULT_SETTINGS["persona"]
assert self.skill.settings.get("model") != DEFAULT_SETTINGS["model"]

if __name__ == "__main__":
pytest.main()
Loading