How to Build a Skill

Before you start coding the skill

  1. Pick a name for the skill. Skill names should be unique so pick something that nobody else will take accidentally.
  2. Create a file in skills/ with the skill name (use snake_case).
  3. Use that file for the skill's code. Whenever the code there changes, you need to rebuild and restart the docker container (docker-compose up -d --build).

Creating the skill

All skill code should be defined inside of a Skill.define block:

Skill.define 'skill name' do
  # Skill code here
end

Directly in the skill's define block, you can attach listeners (SkillActions.listen_for, SkillActions.on_intent, SkillActions.on_reaction), provide Wit.ai examples (SkillActions.teach) or hook into the skill lifecycle (SkillActions.on_start, SkillActions.on_stop).

For all available top-level actions and how to use them, see the class methods on SkillActions.

Interactions and context

Whenever some of the above listeners get triggered, the code provided to them is executed in an interaction. The interaction is a "thread" (specifically, a Fiber / Green Thread) and provides a context for some actions inside of the handler code.

For example, the following code will know where to reply to the user's original ping message:

Skill.define 'skill name' do
  listen_for 'ping' do
    say 'pong'
  end
end

Using this implicit context, the say method knows to either reply in a thread (if on a public channel) or send a direct message (if the ping is a direct message). See SlackMessageAbilities#say for more details on the behaviour.

The interaction context includes the original message that started the interaction, the user who sent that message, what channel it was on, etc.

Abilities

Abilities are tools that skills can use to achieve their goal. In programmer terms, abilities are helper methods providing functionality. See SkillActions and all of its instance methods (including the ones included from modules) for a complete list and documentation on each one.

Some abilities are called directly (as with say above), while others are used through an object (e.g. google_calendar.create_event).

Schedules

Zayo can keep track of time and trigger some interactions at a specified moment.

Skill.define 'ping skill' do
  listen_for 'ping' do
    schedule.at 10.minutes.from_now, :pong
  end

  def pong
    say 'pong'
  end
end

The above skill listens for ping (ping on a DM or zayo, ping in a public channel), then waits 10 minutes and responds with pong.

The original interaction context is preserved and the say method still knows where to send the message.

The schedules are also saved to persistent storage, so even if Zayo is restarted, the schedule will not be lost.

Switching channels

Sometimes you want to send a message to a different channel. This can be done using SkillActions#in_channel.

Accessing persistent storage

See SkillActions#storage. Storage is scoped to a skill, so that skills don't step on their toes. However, SkillActions#storage acts as a transaction so other skills trying to access the storage at the same time must wait. Keep the storage open as less time as possible.

Strings and Internationalization

Some methods (e.g. SlackMessageAbilities#say and SlackQuestionAbilities#ask_string) can accept both strings and symbols as a message argument.

If the argument is a string (say 'hi'), it is sent as-is.

If the argument is a symbol (say :hi), it is used as a key and a message is taken from config/string.yml corresponding to that key (the top-level hi key in this example). The symbols can include ., which denotes a subkey. For example, say :'food.lets_go' looks for the nested key lets_go inside of food. If the key is set to a list instead of a string, one of the elements of the array is taken at random.

Configuring Wit.ai intents

TODO

Print natural language strings for dates

Instead of saying something like You will be reminded at 2019-10-05T10:09:00Z., you can say You will be reminded tomorrow at 10:09 in the morning.. The SkillActions#humanize method can be used to transform dates and times to human-like strings:

Skill.define 'ping skill' do
  listen_for 'ping' do
    ping_time = 10.minutes.from_now

    say "You will be reminded #{humanize(ping_time)}"

    schedule.at ping_time, :pong
  end

  def pong
    say 'pong'
  end
end

I want to learn more!

You can take a look at what abilities there are - see the instance methods of SkillActions.