Skip to content

Built-in Skills

About

File: builtin_skills/about.py

Function Description
get_assistant_version() Returns the installed Kenzy package version

Announce

File: builtin_skills/announce.py

Broadcasts a spoken message to other rooms. Say "Hey Kenzy… tell everyone dinner's ready" and Kenzy speaks it in every room, then confirms in the room you asked from.

Function Description
announce(message, rooms="") Speak message aloud in other rooms; rooms is an optional comma-separated list of room names (empty = everywhere)

This is the first user of the server-actions mechanism: the skill can't speak in other rooms itself (it runs in kenzy-llm), so it queues an action that kenzy-server actuates via its existing announce() (synthesize once, stream to the target nodes). The asking room is excluded from the broadcast so it doesn't hear the message twice. The server tells the model which room names are currently connected, so it targets real rooms.


Calibrate

File: builtin_skills/calibrate.py

"Hey Kenzy, calibrate" — runs the guided audio calibration for the room you're speaking in, entirely by voice: she asks for a few quiet seconds, then the wake word a few times, detects whether your speaker cancels its own echo (and sets hardware_aec accordingly), applies the measured thresholds, and finishes with a live wake-word test. Same flow as the dashboard's Set up / calibrate audio button — see the dashboard guide. Audio device selection stays in the dashboard (a node with the wrong device can't hear you say "calibrate" anyway).

Function Description
calibrate_audio() Start the guided calibration on the asking node
fast_calibrate (fast intent) Instant trigger for the bare phrasings ("calibrate", "recalibrate your hearing", "run calibration")

Date & Time

File: builtin_skills/datetime_skill.py

Answers "what time is it?" and "what's the date today?" instantly — these are handled by a deterministic fast intent with no language-model call, so the most common question of the day is also the fastest. Uses location.timezone from llm.yaml.

Matcher Description
fast_datetime (fast intent) Time and date queries, answered locally with no LLM round-trip

Enroll Speaker

File: builtin_skills/enroll.py

Starts hands-free voice enrollment: "hey Kenzie, enroll me as Alice" — Kenzy reads a few sentences aloud and records your replies through the room's mic.

Function Description
enroll_speaker(name) Requests a server-side enrollment session for name at the asking node

Off by default: the server honors this skill only when allow_voice_enroll is enabled in the speaker service config — when it's on, anyone within earshot can enroll, including under an existing name. See the security discussion and the alternative enrollment paths (dashboard, CLI) in Speaker Enrollment.


Home Assistant

File: builtin_skills/home_assistant.py

Controls and queries smart home devices via the Home Assistant REST API. See Home Assistant for full setup documentation.

Skills

Function Description
handle_home_control(request, speaker) Natural language control of lights, fans, locks, covers, and thermostats

Configuration

Set under skills.home_assistant in llm.yaml:

Key Default Description
url "http://homeassistant.local:8123" Home Assistant base URL
model "gpt-4o" Model used by the fallback resolver
base_url Base URL for local model providers
curation_file "data/home_assistant/curation.yaml" Aliases, notes, room defaults, and exclusions (optional)
cache_ttl 300 Seconds to cache the live HA topology pull
domains light, switch, fan, cover, lock, climate Entity domains exposed to voice control
default_room "" Room assumed when the user does not specify one
thermo_min / thermo_max 65 / 85 Comfort clamp (°F) for relative "make it warmer/cooler" thermostat adjustments

The device inventory is pulled live from Home Assistant — there are no device-map files to maintain. See Home Assistant.

Requires: HA_API_KEY in .env


Intercom

File: builtin_skills/intercom.py

Starts a live two-way voice call to another room. Say "call the living room" and Kenzy rings that room; the call connects only after someone there says "yes" to accept it.

Function Description
connect_room(room) Ring room for a live intercom call (the other room must verbally accept)

Like announce, this queues a server action. The server rings the target room, plays a spoken consent prompt, and bridges audio only on a clear spoken "yes" (default-deny on silence/ambiguity/timeout). During an active call a wake word at either end ends it immediately. Requires a speakerphone with hardware echo cancellation at both ends — a room whose node is marked hardware_aec: false can't join a call (two-way audio without AEC is a feedback loop), and Kenzy politely says so instead of connecting.


Lists

File: builtin_skills/lists.py

Shopping and to-do lists by voice — "add milk to the shopping list", "what's on the list?", "check off eggs", "take bread off the list".

The lists themselves live in Home Assistant (todo entities), not in Kenzy — deliberately: HA's companion app puts the list on everyone's phone (the half of a shopping list that matters at the store), and the same interface covers HA's local lists and synced backends (Google Tasks, Todoist, CalDAV, …) alike. Kenzy is the voice layer.

Setup: you barely need any — with no list yet, saying "add eggs to the shopping list" makes Kenzy ask "Should I create one called Shopping list?"; on your spoken yes it creates a Local to-do list in HA and adds your items (nothing is ever created without the confirmation). If HA won't allow the creation, Kenzy falls back to the manual instruction: Settings → Devices & Services → Add Integration → Local to-do. You can also ask directly — "create a camping list". Then, optionally, in the Kenzy dashboard's Home Assistant tab → Lists**, pick which list a bare "the list" means and add spoken aliases ("the groceries"). With exactly one list it's the default automatically; with several and no default set, Kenzy asks which one — and keeps the mic open for your answer.

The common phrasings are a deterministic fast intent (instant, no model call): adding ("add bread, jam and coffee…" adds three items), reading, removing ("take X off the list" — deletes the item), and checking off ("check off X", "mark X as done" — stays visible as completed in HA). Item names match case-insensitively. Fuzzier requests ("add everything I need for pancakes") fall through to the LLM tools:

Deleting a whole list ("delete the grocery list") is deliberately stricter than everything else, because it's destructive and voice mishears: Kenzy confirms first — "The Grocery list still has 3 items on it. Delete it for good?" — and only your spoken yes deletes it. The name must match exactly (or a configured alias): no fuzzy matching, and a bare "delete the list" asks which one rather than assuming the default. Only Local to-do lists can be deleted; a list synced from an outside service (Google Tasks, Todoist, CalDAV) gets a spoken explanation instead. "Delete milk from the list" still removes just the item.

Function Description
add_to_list(items, list_name) Add one or more items; empty list_name = the default list
read_list(list_name) Read the open items
remove_from_list(items, list_name) Delete items from the list
complete_list_items(items, list_name) Mark items done (kept as completed in HA)
create_list(name, items) Create a new Local to-do list (only on explicit request/confirmation)
delete_list(name) Delete a whole Local to-do list — always confirm-gated, exact name only
fast_lists (fast intent) The instant tier for all the phrasings above

Requires: HA_API_KEY in .env and the Home Assistant skill's url (the lists share its HA connection). Everything here is hard-gated on that connection: with no HA_API_KEY, or with the home_assistant skill disabled in the Skills tab, list commands stay out of the way entirely.


News

File: builtin_skills/news.py

Fetches headlines and article summaries from configurable RSS feeds. Articles are extracted with trafilatura and summarized by a sub-LLM call.

Skills

Function Description
get_news(category) Returns a numbered list of headlines for the given category
get_news_article(category, article_number) Fetches and summarizes a specific article by its position in the list

Configuration

Set under skills.news in llm.yaml:

Key Default Description
max_headlines 5 Maximum headlines returned per request
model "gpt-4o" Model used for article summarization
base_url Base URL for local model providers
feeds (see below) Map of category name → RSS feed URL

Default feeds:

feeds:
  latest:   "https://moxie.foxnews.com/google-publisher/latest.xml"
  world:    "https://moxie.foxnews.com/google-publisher/world.xml"
  politics: "https://moxie.foxnews.com/google-publisher/politics.xml"
  local:    "https://myfox8.com/feed/"

Add or replace any category by editing the feeds map. Any RSS 2.0 or Atom feed works.


Random Tools

File: builtin_skills/random_tools.py

The common bare forms ("flip a coin", "roll a d20", "roll 3d6", "pick a number between 1 and 10") are handled by a fast intent (fast_random) with no model call. Anything with a tail that needs reasoning ("flip a coin to decide whether I should…") falls through to the LLM.

Utility skills for randomness and selection.

Function Description
flip_coin() Returns "heads" or "tails"
roll_dice(sides, count) Rolls one or more dice; sides defaults to 6
pick_number(min, max) Random integer in the given range
pick_from_list(items) Picks one item from a list
yes_no_maybe() Returns "yes", "no", or "maybe"

Note

yes_no_maybe and pick_from_list include explicit docstring clauses that instruct the LLM not to use them for factual or deterministic questions.


Social

File: builtin_skills/social.py

Instant greetings and a conversational bail-out — fast intents, no model call, no network (they work offline).

Matcher Description
fast_greeting (fast intent) "hello", "good morning/afternoon/evening", "goodnight", "howdy", "what's up" → a warm, varied reply (time-specific greetings echo the right part of day)
fast_nevermind (fast intent) "never mind" / "forget it" → "Okay, no problem" — and ends a held multi-turn dialog cleanly

Both match the whole utterance only, so a greeting with a request attached ("hello, turn on the lights") or a list edit ("forget the eggs on the list") falls through to the right skill. "Thanks/thank you" is intentionally left to the LLM — speech-to-text hallucinates those from background noise, so a fast "you're welcome" would fire on phantom transcriptions.


Stocks

File: builtin_skills/stocks.py

Returns stock quotes using yfinance.

Skills

Function Description
get_stock_info(tickers) Price, day range, 52-week range, and percentage change for one or more symbols

Example response

Apple Inc. (AAPL)
  Price: 213.45 USD  +5.32 (+2.56%)  [REGULAR]
  Day range: 208.10 – 214.20
  52-week range: 164.08 – 237.23
  200-day avg: 198.45

Timers, Alarms & Reminders

File: builtin_skills/schedule.py

Set timers, alarms, and spoken reminders by voice. Entries are stored on the server (data/schedules.json) so they survive a restart, and fire as spoken announcements in the room that set them — or another room you name ("wake me at 7 in the bedroom"). Everything is visible and cancellable in the dashboard's Scheduled tab.

One hardware caveat: an alarm's ring loop is silenced by the wake word — heard over the ringing — so alarms need an echo-cancelling speaker. Setting an alarm for a room marked hardware_aec: false is refused with an offer of a timer or reminder instead; an alarm that already exists for such a room still fires, once, timer-style. Timers and reminders work in every room.

Say things like:

  • "set a timer for 10 minutes" · "start a pizza timer for an hour and a half"
  • "how much time is left?" · "cancel the timer" · "what timers do I have?"
  • "wake me at 7" · "set an alarm for 6:30 pm every weekday"
  • "remind me in 20 minutes to flip the bread" · "remind me at 6:15 to take out the trash"
  • "turn on the porch light in 30 seconds" · "lock the front door at 10:30 pm"any command can be deferred: it replays at fire time exactly as if you'd just said it, in the same room

The common phrasings are handled by a deterministic fast intent — instant, no model call. Fuzzier requests ("remind me tomorrow evening…", ambiguous times) fall through to the LLM tools below. A timer plays a tone and announces once when done; an alarm rings repeatedly — tone plus announcement, every ~25 seconds — until you acknowledge it by saying the wake word (capped at ~4 minutes so an empty house isn't lectured); a reminder speaks its text ("You asked me to remind you to take the dog out."). Alarms and clock reminders can recur — every day, weekdays, weekends, or specific days.

The tones are per-room settings (sound_timer / sound_alarm, bundled defaults), editable in each node's dashboard config page like the other sounds — set one empty for voice-only. They're mixed in by the server, so changes apply immediately, and the alarm tone still sounds even if speech synthesis is down. See Node Configuration.

Function Description
set_timer(duration_seconds, label) Countdown timer, optionally named
set_alarm(time, days, room) Clock alarm ("HH:MM"), optional recurrence + room
set_reminder(text, in_seconds, time, days, room) Spoken reminder, relative or clock-based
run_later(command, in_seconds, time) Defer any voice command — replayed through the normal pipeline at fire time
list_schedules() The asking room's active entries (with ids)
cancel_schedules(ids) Cancel entries by id
fast_schedule (fast intent) The instant tier for all the common phrasings above

With multiple timers running, a bare "cancel the timer" asks which one (and holds the mic open for your answer); "cancel my timers" cancels them all. Missed entries (server down at fire time) are spoken late if less than 5 minutes late, otherwise dropped; a missed recurring alarm just moves to its next occurrence.

Deferred commands carry the speaker identity from when they were set, so a voice-gated action ("unlock the door in 10 minutes") is authorized by the voice that scheduled it. They are deliberately one-shot — "turn on the porch light every day at 8" is a standing automation, which is Home Assistant's job, and Kenzy will point you there.


Volume

File: builtin_skills/volume.py

Adjusts the volume of the room you're speaking from — also a deterministic fast intent, so it's instant.

Say things like: "turn it up", "quieter", "set the volume to 40", "volume at 75 percent", "mute", "unmute".

Matcher Description
fast_volume (fast intent) up / down (±15), set to an exact level, mute, unmute — applied to the asking room's node

The volume level persists (it's part of the node's server-owned config, also settable from the dashboard's slider). Mute is temporary — a muted node still plays its wake-word chime at a low level so you can tell it's listening, and comes back un-muted after a restart.


Weather

File: builtin_skills/weather.py

Everyday phrasings ("what's the weather", "temperature outside", "what's the forecast for the week") are fast-dispatched by fast_weather — routed straight to the current/forecast function without the LLM's tool-selection round-trip (faster, though it still fetches from the weather service). A named location ("weather in Paris") or a reasoning question ("should I bring an umbrella?") falls through to the LLM.

Provides current conditions and multi-day forecasts using the National Weather Service API (US only, no API key required). Geocoding uses Nominatim (OpenStreetMap).

Skills

Function Description
get_current_weather(location) Current temperature, conditions, humidity, wind
get_forecast(location, days) Multi-day forecast; days defaults to 3

Configuration

The weather skill has no per-skill keys. When the user doesn't name a location it uses the top-level location: block in llm.yaml (city + state; optional latitude/longitude skip a geocoding step and are otherwise derived from city/state). Output is in °F.


File: builtin_skills/web_search.py

Lets Kenzy search the web for things it can't answer from the language model's own knowledge — recent events, prices, sports scores, opening hours, or any "look it up…" request. The skill returns the top results (title, snippet, source) to the model, which reads them and speaks a concise answer.

Skills

Function Description
web_search(query) Searches the web and returns a numbered list of results for the model to synthesize from

Configuration

Set under skills.web_search in llm.yaml:

Key Default Description
provider "duckduckgo" Search backend: duckduckgo or searxng
max_results 5 Results returned per search
timeout 15 Request timeout in seconds
region "wt-wt" DuckDuckGo region code (wt-wt = no region)
searxng_url "http://localhost:8888/search" Your SearXNG /search endpoint (searxng provider only)

All of these are editable from the dashboard (Services → llm), with the provider as a dropdown.

DuckDuckGo (the default) is keyless and needs zero setup — it just works. If you'd rather no search queries leave your network, point the searxng provider at a self-hosted SearXNG instance; its JSON output format must be enabled (search.formats: [html, json] in SearXNG's settings.yml).

Queries go out, either way something is searched

With DuckDuckGo, the search query (not the room audio) is sent to an external service. Use SearXNG if you self-host your search. Disable the skill entirely (web_search under skills.disabled, or the dashboard's Skills tab) if you don't want Kenzy searching the web at all.