golexa

package module
v0.0.0-...-a59aedb Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 26, 2020 License: MIT Imports: 14 Imported by: 0

README

golexa

golexa is very much an early work in progress. While there's enough here currently to deploy a basic Alexa skill in Go, there are a ton of rough edges that need to be shaved off and features that I need to add before I would not be embarrassed by someone using this.

golexa is a framework that helps you ship Alexa skill code using Go on AWS Lambda. Amazon has official SDKs for Java, Node, and Python, but even though Go is a great language for FaaS workloads like this, they've not released a Go SDK yet. This project tries to fill that gap as much as possible.

Note: golexa does not help you set up your interaction model. You should still set up your skill/model using Amazon's developer console at https://developer.amazon.com/alexa/. golexa only takes the sting out of handling incoming requests and responding w/ as little code as possible.

Getting Started

go get github.com/robsignorelli/golexa

Basic Usage

Let's pretend that you defined an "echo" skill that has these two interactions; one that has Alexa say hello to someone and one that has her say goodbye to someone. In the Alexa console, we might have set up 2 intents with the following sample utterances:

HelloIntent
  Say hello to {name}
  Say hi to {name}

GoodbyeIntent
  Say goodbye to {name}
  Say bye to {name} 

This is all it takes to have a fully-baked skill that handles both interactions.

package main

import (
    "context"

    "github.com/robsignorelli/golexa"
)

func main() {
    skill := golexa.Skill{}

    skill.RouteIntent("HelloIntent", func(ctx context.Context, req golexa.Request) (golexa.Response, error) {
        name := req.Body.Intent.Slots.Resolve("name")
        return golexa.NewResponse(req).Speak("Hello " + name).Ok()
    })
    skill.RouteIntent("GoodbyeIntent", func(ctx context.Context, req golexa.Request) (golexa.Response, error) {
        name := req.Body.Intent.Slots.Resolve("name")
        return golexa.NewResponse(req).Speak("Goodbye " + name).Ok()
    })
	
    golexa.Start(skill)
}

Obviously, you wouldn't normally write all of your code in main(), but it shows you how little you need to write in order to get a working skill. For an ever-so-slightly more complex example, you can look in the sample/ directory for a simple TODO list skill.

Middleware

There are some units of work you want to execute in most/all of your intent handlers. For instance you might want to log every incoming request or validate that a user has linked their Amazon account to your system before doing any real work. All of this can be done using middleware, similar to how you might do this in a REST API.

The golexa framework provides a couple of middleware functions out of the box for logging and making sure that the user has an access token (i.e. set up account linking), but you can easily create your own by implementing a function with the proper signature.

func main() {
    mw := golexa.Middleware{
        middleware.Logger(),
        middleware.RequireAccount(),
        CustomMiddleware
    }
    
    // Log, authorize, and do custom work on the add/remove intents, but not the status intent.
    service := NewFancyService()
	skill := golexa.Skill{}
    skill.RouteIntent("FancyAddIntent", mw.Then(service.Add))
    skill.RouteIntent("FancyRemoveIntent", mw.Then(service.Remove))
    skill.RouteIntent("FancyStatusIntent", service.Status)
    golexa.Start(skill)
}

func CustomMiddleware(ctx context.Context, request Request, next HandlerFunc) (Response, error) {
    // ... do something awesome before the request ...
    res, err := next(ctx, request)
    // ... do something awesome after the request ...
    return res, err
}

Templates

Chances are that most of your intents have some sort of standard format/template for how you want to respond to the user. It's super easy to define speech templates using standard Go templating and evaluate them with whatever runtime data you want. In this example, the .Value of the template context is just a string, but you can make it whatever you want for more complex responses.

greetingHello := speech.NewTemplate("Hello {{.Value}}")
greetingGoodbye := speech.NewTemplate("Goodbye {{.Value}}")

skill := golexa.Skill{}
skill.RouteIntent("GreetingIntent", func(ctx context.Context, req golexa.Request) (golexa.Response, error) {
    name := req.Body.Intent.Slots.Resolve("name")

    switch {
    case shouldSayHello(req):
        return golexa.NewResponse(req).SpeakTemplate(greetingHello, name).Ok()
    default:
        return golexa.NewResponse(req).SpeakTemplate(greetingGoodbye, name).Ok()
    }
})

You can also inject custom functions into your templates to perform more processing as you see fit by passing in any number of WithFunc() options into your NewTemplate() call.

greetingHello := speech.NewTemplate("Hello {{.Value | jumble}}",
    speech.WithFunc("jumble", jumbleText))

...

func jumbleText(value string) string {
    // ... shuffling logic goes here ...
}

Templates: Multi-Language Support

Why limit yourself to just English? Golexa speech templates provide simple hooks to support ANY of the locales/languages that Alexa supports (see full list here).

greetingHello := speech.NewTemplate("Hello {{.Value}}",
    speech.WithTranslation(language.Spanish, `Hola {{.Value}}`),
    speech.WithTranslation(language.Italian, `Ciao {{.Value}}`),
)

Now when you respond w/ this template and the name slot is "Rob", Alexa will say "Hola Rob" when the locale is "es", "es-MX", "es-ES", etc. It will cay "Ciao Rob" when the locale is any locale that starts with "it" and it will fall back to the English translation of "Hello Rob" for any other language.

In this example, I use the same translation for all Spanish variants, but you can just as easily support different language variants, too:

// You could use "language.LatinAmericanSpanish" and "language.EuropeanSpanish"
// instead of parsing, but I wanted to show you the actual locale names.
greetingHello := speech.NewTemplate("Hello {{.Value}}",
    speech.WithTranslation(language.MustParse("es-MX"), `Hola {{.Value}}`),
    speech.WithTranslation(language.MustParse("es-ES"), `Hola from Spain, {{.Value}}`))
)

Back-and-Forth Interactions w/ ElicitSlot

You want your interactions to be as friendly to your users as possible. For instance, you might have an interaction/intent where users might want to add an item to their list that you're maintaining for them. You might want to support both of these phrases:

AddItemIntent
  Add {item_name} to my list
  Update my list

In the first case the user provides the name of the item they want to add, so you have all of the information you need to complete the request. In the latter case, you don't, so you want to have Alexa prompt the user for that information and then trying again. Here's how you can use golexa to fulfill the request when you have everything, and "elicit slot" when you don't.

skill.RouteIntent("AddItemIntent", func(ctx context.Context, req golexa.Request) (golexa.Response, error) {
    itemName := req.Body.Intent.Slots.Resolve("item_name")

    // They said "update my list", so have their device ask them for the
    // item name and send the result back to this intent again.
    if itemName == "" {
        return golexa.NewResponse(req).
            Speak("What would you like me to add to the list?").
            ElicitSlot("AddItemIntent", "item_name").
            Ok()
    }

    // They either specified the "item_name" slot in the initial request or
    // we were redirected back here after an ElicitSlot.
    addItemToList(req.UserID(), itemName)
    return golexa.NewResponse(req).
        Speak("Great! I've added that to your list.").
        Ok()
})

Future Enhancements

Here are a couple of the things I plan to bang away at. If you have any other ideas that could help you in your projects, feel free to add an issue and I'll take a look.

  • Echo Show display template directive support
  • Name free interactions through CanFulfillIntentRequest

Because this is still very much a work in progress, I can't promise that I won't make breaking changes to the API while I'm still trying to shake this stuff out.

Documentation

Index

Constants

View Source
const (
	RequestTypeCanFulfillIntent = "CanFulfillIntentRequest"
	RequestTypeIntent           = "IntentRequest"
	RequestTypeLaunch           = "LaunchRequest"
)
View Source
const (
	IntentNameCancel       = "AMAZON.CancelIntent"
	IntentNameFallback     = "AMAZON.FallbackIntent"
	IntentNameHelp         = "AMAZON.HelpIntent"
	IntentNameNavigateHome = "AMAZON.NavigateHomeIntent"
	IntentNameStop         = "AMAZON.StopIntent"
)

The standard intents that your skill should implement to handle standard Alexa interactions.

Variables

This section is empty.

Functions

func Start

func Start(skill Skill)

Start turns on the appropriate listener to handle incoming requests for the given skill. It will automatically detect if you're just running the process on your local machine or some testing cluster or if you are actually running it as an AWS Lambda function. When not running in Lambda it will simply fire up an HTTP server that listens on port 20123 for equivalent incoming blocks of JSON as you'd receive from the Alexa API to your Lambda function. You can use a different port by setting the GOLEXA_HTTP_PORT environment variable.

You should only call this once per process!

Types

type Application

type Application struct {
	ID string `json:"applicationId,omitempty"`
}

Application identifies the skill whose interaction model was used to invoke this request.

type Device

type Device struct {
	ID                  string                 `json:"deviceId,omitempty"`
	SupportedInterfaces map[string]interface{} `json:"supportedInterfaces"`
}

Device contains information about the type of Echo device that the request came from.

type HandlerFunc

type HandlerFunc func(ctx context.Context, request Request) (Response, error)

HandlerFunc defines a core operation of your skill. It takes the request with all incoming user interaction information and returns a Response w/ instructions for how Alexa should react to the user's utterance/request.

type Middleware

type Middleware []MiddlewareFunc

Middleware represents a chain of units of work to execute before your intent/request handler.

func (Middleware) Then

func (m Middleware) Then(handler HandlerFunc) HandlerFunc

Then creates a wrapper function that forces a request to run through a your gauntlet of middleware functions before finally executing the intent/request handler you're registering.

type MiddlewareFunc

type MiddlewareFunc func(ctx context.Context, request Request, next HandlerFunc) (Response, error)

MiddlewareFunc defines a standardized unit of work that you want to execute after an Alexa request comes in, but before your actual intent handler fires. By simply not invoking the 'next' function, you can short circuit the execution without running your intent handler at all (think restricting access to intents based on whether they're account linked or not).

type Request

type Request struct {
	Version string         `json:"version"`
	Session requestSession `json:"session"`
	Body    requestBody    `json:"request"`
	Context requestContext `json:"context"`
}

Request is the core data structure that encapsulates all of the different pieces of data that the Alexa API provides in their JSON.

This struct is an adaptation of the one provided by: https://github.com/arienmalec/alexa-go

func NewIntentRequest

func NewIntentRequest(intentName string, slots Slots) Request

NewIntentRequest creates a minimal request instance you can use to write unit tests for your intent requests.

func (Request) DeviceID

func (r Request) DeviceID() string

DeviceID traverses the request structure to extract the id of the device making the call.

func (Request) Language

func (r Request) Language() language.Tag

Language parses the incoming 'locale' attribute to determine the language we should use for translating text.

func (Request) SessionID

func (r Request) SessionID() string

SessionID traverses the request structure to extract the id of the Amazon/Alexa session making the call.

func (Request) SkillID

func (r Request) SkillID() string

SkillID returns the id of the skill that was invoked to handle this request.

func (Request) UserAccessToken

func (r Request) UserAccessToken() string

UserAccessToken traverses the request structure to extract the account-linked access token for the caller.

func (Request) UserID

func (r Request) UserID() string

UserID traverses the request structure to extract the id of the Amazon/Alexa user making the call.

type Response

type Response struct {
	Request           Request                `json:"-"`
	Version           string                 `json:"version"`
	SessionAttributes map[string]interface{} `json:"sessionAttributes,omitempty"`
	Body              responseBody           `json:"response"`
}

Response encapsulates all of the various options that your skill can respond with to control the audio and visual aspects of the experience. It is a builder that lets you add on additional pieces to the experience as you deem necessary. For instance you can invoke `Speak()` to control what Alexa will say to the user then call `SimpleCard()` to provide some basic visual feedback.

This struct is an adaptation of the one provided by: https://github.com/arienmalec/alexa-go

Also see https://developer.amazon.com/docs/custom-skills/request-and-response-json-reference.html#response-format

func Fail

func Fail(message string) (Response, error)

Fail should be used in only the most dire of unrecoverable circumstances. It will respond with no Alexa instructions and an error w/ the given message. You should NOT use this in instances where your skill can't give a meaningful response to a question. It should only be used for critical, unexpected paths such as receiving a request for an intent your code doesn't have registered.

func NewResponse

func NewResponse(request Request) Response

NewResponse create a bare-bones response instance that you can continue to expand on with additional instructions for Alexa like `Speak()` or `SimpleCard()`.

func (Response) ElicitSlot

func (r Response) ElicitSlot(intentName, slotName string) Response

ElicitSlot keeps the current session open and has the user's echo device go back into capture mode. Whatever the user speaks next will be applied to the specified slot and all other slots from this request will be sent along to the slot you named. You should use this in conjunction with `Speak()` so that Alexa will give a meaningful prompt rather than just displaying a blue ring.

For example, if you're setting up some sort of game, you might respond with the speech "How many players?" and `ElicitSlot(req, "StartGameIntent", "num_players")`. Once the user tell you how many players then that slot info will be sent to your "StartGameIntent".

func (Response) EndSession

func (r Response) EndSession(flag bool) Response

EndSession indicates whether or not the user is at the end of a dialog w/ Alexa. This defaults to 'true' so that all interactions are terminal unless otherwise specified.

func (Response) Ok

func (r Response) Ok() (Response, error)

Ok simply returns the Response in its current state and a 'nil' error. This is a convenience so that you can build your response at the end of your handlers which require a response and an error.

func (Response) Reprompt

func (r Response) Reprompt(textOrSSML string) Response

Reprompt should be used in conjunction w/ an `ElicitSlot()` call. If the user doesn't say anything when they're asked to fill in one of the slots, this will be a second audio prompt to try to get them to say something. If the user actually responded the first time, they won't actually hear this.

func (Response) SimpleCard

func (r Response) SimpleCard(title, text string) Response

SimpleCard customizes what the user should see on an Echo device that supports a screen or what shows up when they look at their interaction history in the Alexa app.

func (Response) Speak

func (r Response) Speak(textOrSSML string) Response

Speak indicates w/ you want the Alexa voice to dictate back to the user. You can provide plain text or SSML.

func (Response) SpeakTemplate

func (r Response) SpeakTemplate(template speech.Template, value interface{}) Response

type Skill

type Skill struct {
	Name string
	// contains filtered or unexported fields
}

Skill is the root data structure for your program. It wrangles all of the handlers for the different types of requests your skill is expected to encounter.

func (*Skill) CanFulfillIntent

func (skill *Skill) CanFulfillIntent(handlerFunc HandlerFunc)

CanFulfillIntent allows you to support the "pre-flight" CanFulfillIntentRequest if you want to have your skill undergo name free certification.

See: https://developer.amazon.com/docs/custom-skills/understand-name-free-interaction-for-custom-skills.html

func (Skill) Handle

func (skill Skill) Handle(ctx context.Context, request Request) (Response, error)

Handle routes the incoming Alexa request to the correct, registered handler.

func (*Skill) Launch

func (skill *Skill) Launch(handlerFunc HandlerFunc)

Launch registers the handler for when the user utters "Alexa, open XXX" to launch your skill.

func (*Skill) RouteIntent

func (skill *Skill) RouteIntent(intentName string, handlerFunc HandlerFunc)

RouteIntent indicates that any "IntentRequest" with the specified intent name should be handled by the given function.

type Slot

type Slot struct {
	Name        string      `json:"name"`
	Value       string      `json:"value"`
	Resolutions resolutions `json:"resolutions"`
}

Slot is one of placeholder for the important data used in an utterance/query. It can be a very open-ended bit of text as you'd have in an AMAZON.SearchQuery slot or one of the phrases w/ synonyms you set up in a custom slot.

func NewResolvedSlot

func NewResolvedSlot(name, value, resolvedValue string) Slot

NewResolvedSlot is mainly used for faking test data to create a slot that has an uttered value as well as a resolution value.

func NewSlot

func NewSlot(name, value string) Slot

NewSlot is used primarily for faking data in tests. It creates a simple slot/value with no alternate resolution info.

func (Slot) Resolve

func (slot Slot) Resolve() string

Resolve takes into account the synonyms and resolutions, returning the mapped value that the Alexa API thinks we want. If there was no resolution data, you'll simply get back the transcribed text from what the user actually said.

type Slots

type Slots map[string]Slot

Slots represents a set of runtime values that the Alexa interaction model parsed out for you.

func NewSlots

func NewSlots(values ...Slot) Slots

NewSlots creates a Slots map containing entries for all of the individual slot values.

func (Slots) Clone

func (s Slots) Clone() Slots

Clone creates a copy of all of the slots and their RESOLVED values. Typically you use this when you want to include a set of slots in your response w/o modifying the map in the request. Be aware that while it preserves the resolved value, you will lose the resolution authority data from the original.

func (Slots) Resolve

func (s Slots) Resolve(slotName string) string

Resolve locates the specified slot entry and returns its resolved value.

type User

type User struct {
	ID          string `json:"userId"`
	AccessToken string `json:"accessToken,omitempty"`
}

User identifies the Amazon user account that owns the device that the request came from.

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL