Skip to content
Console
Try one

    Keep Your Skill Responsive

    Use SkillKit for cached resources, varied sounds, bounded service calls, background refresh, and optional persistence without copying the plumbing.

    A skill should spend its time answering the person, rather than finding the same files again or waiting too long for a service. SkillKit provides small helpers for this repeated work. Choose only the ones your skill needs.

    Start with Build an Educational Quiz if you’ve not written a skill yet. These examples extend an existing project. Install with python -m pip install --pre "thalovant-skillkit==0.24.2", and declare thalovant-skillkit>=0.24.2 in your project’s dependencies.

    What you are addingUseYour skill still decides
    Many packaged soundsbundled_files and ShuffleBagPoolWhich sound fits the game and which speaker hears it
    Several translated repliesself.speak_varied_dialogThe actual words and when to invite an answer
    An online serviceJsonServiceClient and RequestBudgetThe request, validation and unavailable reply
    A catalog that refreshes occasionallyPeriodicWorkerFreshness, cancellation and safe cache replacement
    Optional Redis/PostgreSQL stateJsonStateStoreRecord validation, ownership and local recovery
    One game or draft per playerSessionStateStoreWhat a turn changes, and when a game is over
    Regional aliases or blocklistsSkillResources.combined_linesWhich languages the feature should accept

    These helpers don’t add a service call to an offline skill. Importing them does not start a worker or connect to storage. OVOS still handles speech, audio, intent routing, conversations and scheduled alarms.

    Imagine a box of animal sound cards. You first list the cards, then draw from the list each time someone asks. You don’t unpack the whole box for every turn.

    from pathlib import Path
    from thalovant_skillkit.assets import bundled_files
    from thalovant_skillkit.selection import ShuffleBagPool
    sound_dir = Path(__file__).parent / "sounds"
    sounds = bundled_files(sound_dir, "*.ogg")
    choices = ShuffleBagPool()
    # Run inside an intent handler after adding at least two sounds to your package:
    # chosen = choices.draw(sounds)[0]
    # self.play_audio(str(chosen))

    Create choices once per skill instance. bundled_files remembers a sorted tuple of filenames; it doesn’t load the audio into memory. There are at most 128 cached directory/pattern combinations. The first call scans the directory; repeated calls reuse the list. Remember to include your sounds in the built wheel.

    Check your result: call bundled_files(sound_dir, "*.ogg") twice, then inspect bundled_files.cache_info(). The second call increases hits. Draw twice from a pool containing two or more distinct files; the choices differ. A one-file pool can only return that file. Selection alone doesn’t play audio.

    For replies, add multiple lines to the usual translated .dialog file and call:

    # Inside an intent handler; translate quiz.correct.dialog in each supported language.
    self.speak_varied_dialog("quiz.correct", {"name": "Sam"})

    Kit remembers the choices while OVOS handles rendering, resource overrides and speech. If you reload an override with different lines, its selection bag is replaced. Ordinary speak_dialog keeps its normal behavior. For multiple players, keep each player’s last sound in session state and pass it as avoid=last_sound. Kit avoids that choice when another is available.

    A timeout is how long one operation may wait. A reply budget is the shared amount of time available for all the operations in that reply. Without a budget, three one-second calls could keep someone waiting for roughly three seconds.

    from thalovant_skillkit.network import JsonServiceClient, RequestBudget
    # Create these once in your skill instance.
    budget = RequestBudget()
    service = JsonServiceClient("garden-watering", "1.0.0", budget=budget)
    # Use your configured service URL. An empty URL means no service is configured.
    service_url = ""
    with budget.limit(2.0):
    result = service.post(service_url, {"question": "does the garden need water?"},
    timeout=1.0)
    if result is None:
    answer = "I cannot check the garden right now."
    else:
    # Validate the service's fields before using them in your real skill.
    answer = str(result.get("answer", "I could not find an answer."))

    Check your result: with the empty URL, result is None immediately and the fallback text is selected. With a configured service, a valid JSON object becomes a dictionary. A failed request, invalid response or exhausted budget returns None; your skill decides how to answer.

    The client sends identifying and request-ID headers, caps responses at 2 MiB by default and attempts each request once. It doesn’t retry POST requests that may already have performed an action. After a failure, it skips that URL for 30 seconds by default so later turns don’t repeatedly wait on the same broken service. Other URLs remain available. The cooldown has at most 128 entries. Successful responses aren’t cached or shared between speakers.

    Deadlines are separate for concurrent threads, and a nested budget can’t extend its parent. They are checked between reads; they can’t forcibly interrupt a blocked socket or DNS lookup. Always use a finite transport timeout too. A stalled read can finish up to one socket timeout after the budget expires.

    For a skill that loads offline but needs internet for some features, use these class attributes instead of copying a RuntimeRequirements property:

    REQUIRES_INTERNET = True
    INTERNET_BEFORE_LOAD = False
    NO_INTERNET_FALLBACK = True

    Your skill must actually implement its offline reply. These flags describe that behavior to OVOS; they don’t create the reply.

    News refreshes audio addresses in the background. A listener can then use a recent address without waiting for every feed to be checked again. The same pattern suits a small public catalog that can tolerate a little age.

    from thalovant_skillkit.workers import PeriodicWorker
    # In your skill's initialize(), after any required super().initialize():
    self.refresh = PeriodicWorker(self.refresh_catalog, interval=600,
    name="garden-catalog-refresh")
    self.refresh.start()
    # In shutdown(), before super().shutdown():
    self.refresh.stop(timeout=1.0)

    Implement refresh_catalog(self, stop_event). Bound each network operation, check stop_event.is_set() between items, and replace a completed snapshot only after validating it. Keep locks short; don’t hold a cache lock while downloading.

    Check your result: a first refresh runs immediately. The next begins 600 seconds after that pass finishes. Calling start() again doesn’t create a second worker. Exceptions end that pass and are logged; the next pass waits the normal interval. stop() interrupts the idle wait and reports whether the thread exited. It can’t kill a callback that ignores cancellation or blocks indefinitely.

    Use OVOS’s scheduler for a user’s timer or alarm. This worker is for optional refresh work, not exact deadlines. The skill decides how long data remains usable and what to say if it’s too old. Don’t put private details in exception messages.

    Use JsonStateStore when your skill needs optional shared storage. With no backend configured, a new store returns None from load() so your skill can use its local fallback.

    from thalovant_skillkit.storage import JsonStateStore
    store = JsonStateStore("household", {}, env_prefix="GARDEN", default_key="watering")
    records = store.load()

    Install redis for Redis/Sentinel and psycopg for PostgreSQL if you use them. Settings take precedence over the skill’s prefixed environment, then generic backend environment variables. Check the result of each save: configuring shared storage alone doesn’t guarantee that a record was saved. Your skill retains local recovery, validates records and checks who owns them. Don’t use this helper to bypass a service’s own storage API.

    See the storage contract for configuration keys, settings, return values, and connection failure behavior.

    Two people can play the quiz at once, and one shared score would mix their games. SessionStateStore keeps one entry per session ID: the skill reads the ID from the message and stores that player’s game under it.

    from thalovant_skillkit.sessions import SessionStateStore
    # Once per skill instance: at most 128 players, forgotten after 90 quiet seconds.
    self.games = SessionStateStore(max_entries=128, default_ttl=90)
    # In a turn:
    with self.games.lock:
    game = self.games.get(session_id)
    ...
    self.games[session_id] = updated_game

    Writing an entry refreshes its countdown; reading doesn’t. When the store is full, the oldest entry goes. Expiry is checked when the store is used, so nothing runs in the background. The store belongs to one skill instance; a second instance doesn’t see its games. Keep the lock for the decision, not for a network call: the quiz reserves the player’s turn, releases the lock while it fetches questions, then checks that Stop didn’t arrive in the meantime.

    Check your result: start two games with different session IDs and answer one; the other’s score is unchanged. Advance a clock past the TTL and the game is gone. Read the tutorial’s game.py for the complete version.

    Cached dataWhen it changesHow to refresh
    Bundled sound filenamesYou install or edit package assetsA process restart, or bundled_files.cache_clear() between turns
    Locale text and combined aliasesYou install or edit translationsA new resource instance, or self.locale_resources.clear_cache() between turns; a game already in progress keeps the questions it started with
    Dialog/sound selection historyChoices or their explicit key changeKit replaces a changed pool; eviction forgets history only
    Service failure cooldownIts interval expiresKit permits a new request automatically
    Service replies or personal stateDepends on the applicationNo generic reply cache is added; define a scoped freshness policy if needed

    combined_lines(lang, folder, filename) collects accepted aliases or patterns in language preference order, including the configured default. Its combined-result cache holds at most 512 entries. Spoken dialogs should use the usual dialog lookup so they answer in one language. Cache clearing doesn’t coordinate simultaneous file edits: reload between turns. Use ordinary directory reads for live user uploads, not the bundled-file cache.

    matches_literal_intent also caches the normalised phrases for each resource and compatible language set. Reuse self.locale_resources between turns and pass a fixed resource name, such as pocket_quiz.start. You do not need a second cache in your skill. clear_cache() clears these matches and speech-twin lookups too after a resource edit.

    Measure the same action before and after your change with the same Python and OVOS versions. Warm the cache first, repeat the action, and compare several runs without other tests competing for CPU. Also measure a fresh process: a fast warm request can still have a slow startup.

    Separate local selection time from service, speech synthesis and speaker latency. Caching a sound list makes selection faster; it doesn’t make a slow audio device start sooner. Test both cache hits and invalidation, and use a network-disabled benchmark for local work. The SkillKit reference and audio checks cover helper contracts and the additional checks for real spoken turns.