Skip to content
Console

Build an Educational Quiz

Build Pocket Quiz, a voice game that asks a question, offers three choices, and explains the answer. A wrong answer is another chance to learn. You will make it work in English and French, add friendly sounds, and keep each player’s score separate.

You do not need to be an expert. Each lesson adds one idea to a working program. Read the explanation, run the commands, check the result, then try a small change of your own.

Here is part of the finished game. These are spoken words, not terminal commands:

You: start pocket quiz
Quiz: Let’s play Pocket Quiz! Answer with just one, two, or three.
Quiz: Question 1. How many legs does a butterfly have?
One: Four. Two: Six. Three: Eight.
You: two
Quiz: That’s right! Butterflies are insects, and insects have six legs.
Say next for the next question, or again to hear this one.

The finished skill also explains wrong answers, repeats a question, reports a score, and handles Stop. One player can use French while another uses English. Their games stay separate.

Lesson Add Check your result
1 A reply See your first sentence and change it.
2 English and French commands Ask the same skill in two languages.
3 A game, explanations and sounds Finish three questions and see your score.
4 An optional question service Keep playing even when the service is unavailable.

Then you will run tests through OVOS and build an installable package.

You need a computer, Python 3.11 or newer, a text editor, and a terminal. A terminal is the window where you type commands such as python3 --version.

These commands use Bash on Linux or macOS. On Windows, use a Linux shell through WSL. The local lessons need no Thalovant account, hub, microphone, or service token. Installing Python packages needs internet access. The final spoken check needs a test hub and a connected voice client.

This tutorial uses SkillKit 0.11.0, a published version. The project pins that version so the lesson’s code and results stay together. OVOS is the part that listens for commands and sends speech or audio. SkillKit gives our code useful building blocks for working with OVOS.

Download Pocket Quiz and all four lessons. Extract the ZIP, then open a terminal inside the extracted skillkit-pocket-quiz folder.

Check Python, save this folder’s name, and create a virtual environment:

Terminal window
python3 --version
quiz_tutorial="$(pwd)"
python3 -m venv .venv
source .venv/bin/activate

A virtual environment is a separate place for this project’s Python packages. The last command turns it on. Keep this terminal open for the lessons. The quiz_tutorial variable is a bookmark we will use to return to this folder.

Your folder contains the completed skill, its tests, and checkpoints.py. That small script creates a fresh copy for each lesson. It never overwrites a folder that already exists, so your experiments are safe.

Create the first lesson and install it in your active environment:

Terminal window
python checkpoints.py 01 work/01/thalovant-skill-pocket-quiz
cd work/01/thalovant-skill-pocket-quiz
python -m pip install --pre -e ".[test]"
python -m pytest -q

pip installs Python packages. Here, -e means editable: Python reads the code from the folder you are editing. [test] adds the tools used by the tests. --pre allows the OVOS prerelease versions used by this example.

A test is a small program that checks another program. When a line starts with assert, it means “this must be true.” The command above should report passing tests.

See the first reply:

Terminal window
python -c 'from thalovant_skill_pocket_quiz import PocketQuizSkill; print(PocketQuizSkill().preview_reply("", "en-US"))'
Pocket Quiz asks three little questions.

Open thalovant_skill_pocket_quiz/__init__.py. The reply method returns that sentence. Replace its return line with this one, keeping the spaces before it:

return "Welcome to my learning quiz. We will discover something new!"

Save the file, then ask for the preview again:

Terminal window
python -c 'from thalovant_skill_pocket_quiz import PocketQuizSkill; print(PocketQuizSkill().preview_reply("", "en-US"))'

Check your result: the terminal prints your new sentence. A preview returns text; it does not start a game or play sound.

Your test still expects the old sentence. Open tests/test_lesson.py and replace the test_first_reply function with:

def test_first_reply():
assert PocketQuizSkill().preview_reply() == (
"Welcome to my learning quiz. We will discover something new!"
)

Run python -m pytest -q again. Check your result: the tests pass with the new reply. When you deliberately change what a program says, its test should describe the new result too.

What you learned: return gives back a value, print shows it, and a test checks that it is the value you intended.

The later lessons start from fresh copies. They will not copy this experiment automatically. Keep it here, or make the same change in a later lesson.

Return to the download folder and create lesson 2:

Terminal window
cd "$quiz_tutorial"
python checkpoints.py 02 work/02/thalovant-skill-pocket-quiz
cd work/02/thalovant-skill-pocket-quiz
python -m pip install --pre -e ".[test]"
python -m pytest -q

This lesson adds an intent: a phrase that asks the skill to do a job. An intent handler is the Python method OVOS calls when it recognizes that phrase.

It also moves the reply into a dialog file: a plain text file containing what the skill can say. A resource is a file the program reads, such as a dialog, question list, or sound. Open thalovant_skill_pocket_quiz/locale/en-US/dialog/preview.dialog to see the English reply and its fr-FR counterpart to see the French one.

Open the start-intent file in each language:

thalovant_skill_pocket_quiz/locale/en-US/intents/pocket_quiz.start.intent
thalovant_skill_pocket_quiz/locale/fr-FR/intents/pocket_quiz.start.intent

The English file includes start pocket quiz. The French file includes démarre pocket quiz. Each line is an example of what someone can say.

A locale combines a language and a region. en-US is English for the United States; fr-FR is French for France. The folders contain translated words, while the same Python method handles both languages.

Open thalovant_skill_pocket_quiz/__init__.py and find handle_start. Follow three actions: read the message’s language, choose that language’s reply, and ask OVOS to speak it. A message carries the words, the language, and information about the player.

Compare the two preview replies:

Terminal window
python - <<'PY'
from thalovant_skill_pocket_quiz import PocketQuizSkill
skill = PocketQuizSkill()
print(skill.preview_reply("", "en-US"))
print(skill.preview_reply("", "fr-FR"))
PY

Check your result: these two lines appear. The lesson’s tests also send each start command to its handler and check the language of the answer.

Pocket Quiz asks three little questions. Say start pocket quiz to play.
Pocket Quiz pose trois petites questions. Dis démarre pocket quiz pour jouer.

Try a change: add begin pocket quiz on a new line in the English intent file. Create tests/test_my_phrase.py with this complete test:

from thalovant_skill_pocket_quiz import PocketQuizSkill
def test_my_new_start_phrase():
skill = PocketQuizSkill()
assert skill.locale_resources.matches_literal_intent(
"begin pocket quiz", "pocket_quiz.start", "en-US"
)

Run python -m pytest tests/test_my_phrase.py -q. Check your result: one test passes. This checks that the exact phrase was saved in the right resource; the OVOS routing tests later check how spoken commands reach the skill. Keep the skill’s name in the phrase so it has a clear job among other skills.

What you learned: language comes from the incoming message. Hard-coding English in a handler would ignore the person who asked in French.

Create the game lesson:

Terminal window
cd "$quiz_tutorial"
python checkpoints.py 03 work/03/thalovant-skill-pocket-quiz
cd work/03/thalovant-skill-pocket-quiz
python -m pip install --pre -e ".[test]"
python -m thalovant_skill_pocket_quiz.replay
python -m thalovant_skill_pocket_quiz.replay --lang fr-FR
python -m pytest -q

Check your result: each replay prints a three-question game, an explanation after each answer, sound-cue names, and a final score. The English built-in replay answers two, one, then three and finishes with 3 out of 3. The French replay uses deux, un, and trois.

A replay feeds example messages into the game and prints its answers. This is a quick way to see the logic. It does not prove that a microphone or OVOS recognized your words; we test OVOS separately below.

The complete program has three small jobs. These paths are inside the thalovant_skill_pocket_quiz package folder:

File Job
__init__.py Connect the game to OVOS: receive commands, speak, play cues, and stop.
game.py Keep the questions and score, and decide the next reply.
locale/<language>/questions.json Store the questions, choices, correct answers, and explanations.

Open the first question in locale/en-US/questions.json:

{
"question": "How many legs does a butterfly have?",
"choices": ["Four", "Six", "Eight"],
"answer": 1,
"explanation": "Butterflies are insects, and insects have six legs."
}

Python counts list positions from zero. Position 0 is Four, position 1 is Six, and position 2 is Eight. The player says “two” for the second choice; the program compares it with position 1.

In game.py, follow Quiz.turn: it finds this player’s game, checks the answer, updates the score, then returns a Turn. A Turn describes the words, language and sounds to send back. The OVOS handler delivers them.

Try a change: replace one question and its explanation in both language files. Keep three choices and an answer position from 0 to 2. Update the test that checks that question, then run the replay again.

Imagine Alex has answered two questions while Sam has just started. One shared score would mix their games together.

A session ID is the name on each player’s scorecard. The skill reads it from the message. SessionStateStore keeps one game for each ID. It belongs to this skill instance, so a second copy of the skill does not borrow its games.

The store also has limits. By default, a game is forgotten 90 seconds after it starts or after the player’s last answer or “next.” Repeating a question or asking for the score does not restart that countdown. At most 128 games are kept; if another player starts, the oldest saved game is removed. These rules prevent old games from using memory forever. Expiry is checked when the store is used; it does not run an alarm in the background.

Look for session_key, accepts, and turn in game.py. The skill accepts complete answers such as one, two, or three. It leaves set a timer for one minute to another skill. Matching one word anywhere in a sentence would be too broad.

Check your result: the tests cover two players, an old game expiring, and an unrelated command being declined. A missing player ID gets a clear reply instead of sharing an anonymous scorecard.

stop_session removes one player’s game. stop clears all games when OVOS requests a global stop. The comments in __init__.py explain why the session method reports success even when that player’s game has already gone away.

Check your result: the Stop test starts two games, stops one, and checks that the other still exists. No preview creates a scorecard or plays a sound.

The skill includes three original WAV files. A WAV file stores sound samples: small measurements of a sound wave over time.

Cue Meaning
correct.wav You found the answer.
try-again.wav An incorrect answer: listen to the explanation, then say next.
finish.wav The quiz is complete.

Listen to the cues here. They play only when you press Play.

Correct answer

Incorrect answer

Finished quiz

To hear them during the terminal replay, use:

Terminal window
python -m thalovant_skill_pocket_quiz.replay --play-sounds

This optional command uses your computer’s audio output and an available audio player. The words still print as text. The ordinary replay and automated tests stay silent. On a hub, the skill uses OVOS playback and preserves the message’s player information so the cues and spoken reply reach the same client.

The sounds_enabled setting turns the cues on or off. Sound supports the lesson; the spoken explanation still tells the player what happened.

Open tools/generate_sounds.py if you want to understand how the cues were made. It explains pitch, time, loudness and fades. You can change the notes and regenerate the files without downloading sound effects.

The built-in questions work by themselves. This optional lesson shows how to ask another program for questions and keep the quiz useful when that program cannot answer.

Terminal window
cd "$quiz_tutorial"
python checkpoints.py 04 work/04/thalovant-skill-pocket-quiz
cd work/04/thalovant-skill-pocket-quiz
python -m pip install --pre -e ".[test]"
python -m pytest -q

A service is another program your skill can ask for information. Open thalovant_skill_pocket_quiz/provider.py. It uses request_headers to identify the request and post_json to send it with a time limit. It checks the returned questions before using them.

The example permits one request attempt. Empty, malformed or unavailable answers fall back to the packaged questions. The game also checks whether the player stopped or started another game while it was waiting. An old answer must not bring back a stopped game.

Keep the lesson terminal open. Open a second terminal inside the original extracted skillkit-pocket-quiz folder, the one containing .venv and checkpoints.py. Activate the environment, enter lesson 4, and start the demo service:

Terminal window
source .venv/bin/activate
cd work/04/thalovant-skill-pocket-quiz
python -m thalovant_skill_pocket_quiz.demo_provider --port 8765

Check your result: it prints Question server: http://127.0.0.1:8765/questions. The terminal stays busy because it is waiting for requests. It serves only this local demo; you need no account or secret.

Back in the first terminal, run:

Terminal window
python -m thalovant_skill_pocket_quiz.replay --provider http://127.0.0.1:8765/questions

Check your result: the first question becomes “Which animal is a mammal?” The correct answer is the whale, and the explanation says whales breathe air and feed their babies milk. The replay still finishes with 3 out of 3.

Stop the demo service with Ctrl+C in its terminal. Run the same replay command again.

Check your result: the quiz returns to its packaged butterfly, Earth and ice questions. The tests also check badly shaped data and a late answer after Stop.

What you learned: a timeout limits waiting; validation checks whether an answer has the shape you need; a fallback gives the player a useful result when the extra service fails.

The game tests answer “does our code choose the right result?” Native OVOS tests answer “does OVOS deliver the command and emit the right speech and audio?”

The [test] packages you installed include OVOScope, a tool for exercising an assistant without a physical speaker. Run the native tests included in the project:

Terminal window
python -m pytest tests/test_ovos.py -q

These tests initialize the real skill and preserve the player’s session and language. The sound checks inspect playback requests without playing them on the test machine. Tests for the utterance pipeline check recognized start commands and follow-up answers.

Use the testing handbook when you need more detail about test buses, the scheduler, or MiniCroft. A passing test does not measure microphone recognition or how a translated voice sounds in a room.

A wheel is the installable package. It must contain the Python files and the questions, translated replies and sounds they need. A source distribution contains the source needed to build that wheel.

From the lesson 4 project folder:

Terminal window
thalovant-skillkit check
python -m build
thalovant-skillkit check-artifacts . \
--wheel dist/thalovant_skill_pocket_quiz-0.1.0-py3-none-any.whl \
--sdist dist/thalovant_skill_pocket_quiz-0.1.0.tar.gz \
--source tools --wheel-exclude tools \
--wheel-count 'thalovant_skill_pocket_quiz/sounds/*.wav=3'

Check your result: the build creates both files in dist, and the artifact check reports a match with your source. The sound generator belongs in the source archive; the three finished sounds belong in the wheel.

The included .github/workflows/test.yml runs these checks automatically when you push the project to GitHub. This is continuous integration, often called CI. It checks your work; it does not publish a release or install the skill on a hub.

The full check also compares intent examples with the Thalovant fleet. If it reports a local model check as skipped, that part has not passed. Restore model access before calling the fleet check complete. Use check --no-fleet only when you explicitly want the offline source checks.

Check that the wheel also works away from your project folder. These commands create a temporary environment, install the wheel there, and check the installed class, French reply, and three sounds:

Terminal window
quiz_lesson="$(pwd)"
quiz_wheel="$quiz_lesson/dist/thalovant_skill_pocket_quiz-0.1.0-py3-none-any.whl"
quiz_smoke="$(mktemp -d)"
python -m venv "$quiz_smoke/venv"
"$quiz_smoke/venv/bin/python" -m pip install --pre "$quiz_wheel"
cd "$quiz_smoke"
"$quiz_smoke/venv/bin/python" - <<'PY'
from importlib.metadata import entry_points
from importlib.resources import files
plugin, = entry_points(group="opm.skill", name="thalovant-skill-pocket-quiz")
skill_class = plugin.load()
skill = skill_class()
assert "trois" in skill.preview_reply("", "fr-FR")
sounds = files("thalovant_skill_pocket_quiz").joinpath("sounds")
for name in ("correct.wav", "try-again.wav", "finish.wav"):
assert sounds.joinpath(name).read_bytes().startswith(b"RIFF")
print("Installed skill, French reply, and three sound files verified.")
PY
cd "$quiz_lesson"

Check your result: the final Python line prints “Installed skill, French reply, and three sound files verified.” No sounds play during this check. The entry point is the name Python uses to find the installed skill class. This separate install catches files that existed in your editor but were left out of the wheel.

Follow Add Your Own Skill to install from a published Python package or your own Git repository. That flow installs into the skill set you choose; every hub using it receives the skill. A folder on your laptop is not an address the hub can fetch. Wait for the installed status before testing.

Ask start pocket quiz, answer two, then say next. Repeat the game in French with démarre pocket quiz. Check the explanations, sound level, score, and Stop with your voice client. Test two clients independently before sharing the skill with other people.

Inside thalovant_skill_pocket_quiz, copy the complete locale/en-US folder to a new language folder, such as locale/es-ES for Spanish. Translate the question data, dialog files, intent phrases, and short answer vocabulary. Keep names inside braces, such as {score}, unchanged: Python fills them with the game’s values. Add es-ES to locale/supported.json.

Then add tests that start the game, answer, repeat, finish and stop in that language. Check every rendered explanation and score. Ask a fluent speaker to review the words and listen to the voice before advertising support.

The same sound cues can work across languages. Spoken words and explanations still need real translation; copying an English folder alone does not provide it.

  • All four lessons run and their tests pass.
  • English and French games explain both correct and wrong answers.
  • Two players keep separate scores, and Stop affects the intended player.
  • Cues play when enabled; preview and ordinary tests stay silent.
  • Service failure leaves a working local quiz.
  • Source, native OVOS and built-package checks pass.
  • Your test hub answers and plays the expected cue through the intended voice client.

You have used SkillKit’s message, language, resource, text, settings, conversation-state, preview, service, testing and packaging helpers in one skill. Media playback providers and broader fallback skills use different base classes; the handbook explains those choices.

What you see Next action
python3 is not found Install Python, reopen the terminal, and check python3 --version.
The lesson folder already exists Choose a fresh destination. The checkpoint script protects your previous work.
Python imports an earlier lesson Activate this tutorial’s environment, enter the current lesson folder, and install -e ".[test]" again.
A resource change seems ignored Restart the replay or test. Resource helpers keep a cache inside each running instance.
The demo service cannot use port 8765 Choose another port and use the same number in the replay URL.
The terminal replay has no sound Use --play-sounds and check the PC’s output. The normal replay is intentionally silent.
A French reply appears in English Check the message language and the French resource files.
The hub cannot find the skill Check its installation status, source version, and package entry point.