背景
昨年のAWS re:InventでアナウンスされたLambdaのGoサポートがリリースされ、これを機にGoに触れてみたいと思いつつ、Alexaも日本でリリースされてから作ってなかったので、Go/LambdaでAlexaスキルを作ってみることにしました。SAMを使ったブログ「Announcing Go Support for AWS Lambda」がリリースと同時にポストされましたが、今回はServerless Frameworkを使うことにしました。
作ったもの
好きな色を答えさせるサンプルスキルであるalexa-skills-kit-color-expert-nodejs
とalexa-skills-kit-color-expert-python
をGoで書き換えてみることを今回やってみました。言語を替えての写経となりました^^;
最終的な成果物はGitHub(yamaryu0508/alexa-skills-kit-color-expert-go)にアップしています。
作成手順
今回の作成手順を辿っていきます。次のものはインストール・アカウント設定済みとします。
- Go言語開発環境
- AWS CLI
- Serverless Framework
Serverless Frameworkの設定
「Serverless Framework example for Golang and Lambda」を参考に進めます。
まずはサービス作成です。
$ serverless create -u https://github.com/serverless/serverless-golang/ -p alexa-skills-kit-color-expert-go
ディレクトリを移動しておきます。
$ cd alexa-skills-kit-color-expert-go
出来上がった serverless.yml
を整えます。
service: alexa-skills-kit-color-expert-go
provider:
name: aws
runtime: go1.x
stage: dev
region: ap-northeast-1
package:
exclude:
- ./**
include:
- ./bin/**
functions:
ask-go:
handler: bin/main
memorySize: 256
timeout: 10
events:
- alexaSkill
serverlessの準備はこれでOKです。
Alexaスキルの作成 (Goコーディング)
Go/Lambdaを書くために、まずはgithub.com/aws/aws-lambda-go/lambda
をgo get
します。
$ go get github.com/aws/aws-lambda-go/lambda
そして、まず最初にevent
オブジェクトを入れずにレスポンスが思い通りに返せるかを簡単なコードで確認しました。
package main
import (
"errors"
"github.com/aws/aws-lambda-go/lambda"
)
// Response is object-type
type Response struct {
Version string `json:"version"`
Response struct {
ShouldEndSession bool `json:"shouldEndSession"`
OutputSpeech struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"outputSpeech"`
} `json:"response"`
}
// Handler is main
func Handler() (Response, error) {
return Response{
Version: "1.0",
Response: {
ShouldEndSession: true,
OutputSpeech: {
Type: "PlainText",
Text: "Hello",
},
},
}, nil
}
func main() {
lambda.Start(Handler)
}
サンプルやLambdaのGoモデルのドキュメントの見様見真似で書いてみましたが、返したいJSONに対応する構造体をreturn
すれば良いことをここでまず理解しました。同時にHandler
への引数もAlexaのリクエストのJSONに対応する構造体になることに気づきました(Javaもこんな感じなんだろうが、触ったことない)。Lambdaへのトリガーサービスから渡ってくるevent
オブジェクトについては、github.com/aws/aws-lambda-go/events
でAPI Gateway等幾つか定義されているので、ここで定義されているものはそのまま使うと良いかと思います。ただ、Alexaのモデルは定義されてなかったので、自分で書いてあげる必要があります(request.go
に相当)。
ということで、とりあえずレスポンスを返すことに成功したところで、写経を進めていきます(確認方法は後述のコンパイルとデプロイと同様です)。
リファレンス「カスタムスキルのJSONインターフェースのリファレンス」を確認しながらAlexaスキルのリクエストとレスポンスの構造体をそれぞれ準備します。今回は写経に必要ないobject
を結構省いています(音声再生のためのSSML
の扱い等)ので網羅的には出来ていません。
また、データ構造をJSONと整合させるための定義なので、JSONをGoの構造体に変換してくれる「JSON-to-Go」等を使いながら作業を進めると良さそうです。
package alexa
import "time"
/*
* define structs for Alexa Request
*/
// Session is object-type
type Session struct {
New bool `json:"new"`
SessionID string `json:"sessionId"`
Application struct {
ApplicationID string `json:"applicationId"`
} `json:"application"`
Attributes map[string]interface{} `json:"attributes"`
User struct {
UserID string `json:"userId"`
Permissions struct {
ConsentToken string `json:"consentToken"`
} `json:"permissions"`
AccessToken string `json:"accessToken"`
} `json:"user"`
}
// Slot is object-type
type Slot struct {
Name string `json:"name"`
Value string `json:"value"`
ConfirmationStatus string `json:"confirmationStatus"`
Resolutions map[string]interface{} `json:"resolutions"`
}
// RequestIntent is object-type
type RequestIntent struct {
Name string `json:"name"`
ConfirmationStatus string `json:"confirmationStatus"`
Slots map[string]Slot
}
// RequestDetail is map-type
type RequestDetail struct {
Locale string `json:"locale"`
Timestamp time.Time `json:"timestamp"`
Type string `json:"type"`
RequestID string `json:"requestId"`
Intent RequestIntent `json:"intent"`
}
// Request is object-type
type Request struct {
Version string `json:"version"`
Session Session `json:"session"`
Context struct {
System struct {
Application struct {
ApplicationID string `json:"applicationId"`
} `json:"application"`
User struct {
UserID string `json:"userId"`
Permissions struct {
ConsentToken string `json:"consentToken"`
} `json:"permissions"`
AccessToken string `json:"accessToken"`
} `json:"user"`
Device struct {
DeviceID string `json:"deviceId"`
SupportedInterfaces struct {
AudioPlayer struct {
} `json:"AudioPlayer"`
} `json:"supportedInterfaces"`
} `json:"device"`
APIEndpoint string `json:"apiEndpoint"`
} `json:"System"`
AudioPlayer struct {
Token string `json:"token"`
OffsetInMilliseconds float32 `json:"offsetInMilliseconds"`
PlayerActivity string `json:"playerActivity"`
} `json:"AudioPlayer"`
} `json:"context"`
Request RequestDetail `json:"request"`
}
レスポンスについては、alexa-skills-kit-color-expert-python
を参考にBuildSpeechletResponse
やBuildResponse
と言った関数も定義しました。
package alexa
/*
* define structs for Alexa Response
*/
// OutputSpeech is object-type
type OutputSpeech struct {
Type string `json:"type"`
Text string `json:"text"`
}
// Card is object-type
type Card struct {
Type string `json:"type"`
Title string `json:"title"`
Content string `json:"content"`
}
// Reprompt is object-type
type Reprompt struct {
OutputSpeech OutputSpeech `json:"outputSpeech"`
}
// SpeechletResponse is object-type
type SpeechletResponse struct {
ShouldEndSession bool `json:"shouldEndSession"`
OutputSpeech OutputSpeech `json:"outputSpeech"`
Card Card `json:"card"`
Reprompt Reprompt `json:"reprompt"`
}
// SessionAttributes is map-type
type SessionAttributes map[string]interface{}
// Response is object-type
type Response struct {
Version string `json:"version"`
SessionAttributes SessionAttributes `json:"sessionAttributes"`
Response SpeechletResponse `json:"response"`
}
// BuildSpeechletResponse is function-type
func BuildSpeechletResponse(title string, output string, repromptText string, shouldEndSession bool) SpeechletResponse {
return SpeechletResponse{
ShouldEndSession: shouldEndSession,
OutputSpeech: OutputSpeech{
Type: "PlainText",
Text: output,
},
Card: Card{
Type: "Simple",
Title: "SessionSpeechlet - " + title,
Content: "SessionSpeechlet - " + output,
},
Reprompt: Reprompt{
OutputSpeech: OutputSpeech{
Type: "PlainText",
Text: repromptText,
},
},
}
}
// BuildResponse is function-type
func BuildResponse(sessionAttributes SessionAttributes, speechletResponse SpeechletResponse) Response {
return Response{
Version: "1.0",
Response: speechletResponse,
SessionAttributes: sessionAttributes,
}
}
そして、リクエストとレスポンスが定義できたら、Alexaスキルのメイン部分の写経に入ります。
package main
import (
"errors"
"fmt"
"./alexa"
"github.com/aws/aws-lambda-go/lambda"
)
var (
// ErrInvalidIntent is error-object
ErrInvalidIntent = errors.New("Invalid intent")
)
/*
* Functions that control the skill's behavior
*/
// GetWelcomeResponse is function-type
func GetWelcomeResponse() alexa.Response {
sessionAttributes := make(map[string]interface{})
cardTitle := "Welcome"
speechOutput := "Welcome to the Alexa Skills Kit sample. Please tell me your favorite color by saying, my favorite color is red"
repromptText := "Please tell me your favorite color by saying, my favorite color is red."
shouldEndSession := false
return alexa.BuildResponse(sessionAttributes, alexa.BuildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession))
}
// HandleSessionEndRequest is function-type
func HandleSessionEndRequest() alexa.Response {
sessionAttributes := make(map[string]interface{})
cardTitle := "Session Ended"
speechOutput := "Thank you for trying the Alexa Skills Kit sample. Have a nice day! "
repromptText := ""
shouldEndSession := true
return alexa.BuildResponse(sessionAttributes, alexa.BuildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession))
}
// CreateFavoriteColorAttributes is function-type
func CreateFavoriteColorAttributes(favoriteColor string) alexa.SessionAttributes {
sessionAttributes := make(map[string]interface{})
sessionAttributes["favoriteColor"] = favoriteColor
return sessionAttributes
}
// SetColorInSession is function-type
func SetColorInSession(intent alexa.RequestIntent, session alexa.Session) alexa.Response {
cardTitle := intent.Name
sessionAttributes := make(map[string]interface{})
shouldEndSession := false
speechOutput := ""
repromptText := ""
if color, ok := intent.Slots["Color"]; ok {
favoriteColor := color.Value
sessionAttributes = CreateFavoriteColorAttributes(favoriteColor)
speechOutput = "I now know your favorite color is " + favoriteColor +
". You can ask me your favorite color by saying, " +
"what's my favorite color?"
repromptText = "You can ask me your favorite color by saying, " +
"what's my favorite color?"
} else {
speechOutput = "I'm not sure what your favorite color is. " +
"Please try again."
repromptText = "I'm not sure what your favorite color is. " +
"You can tell me your favorite color by saying, " +
"my favorite color is red."
}
return alexa.BuildResponse(sessionAttributes, alexa.BuildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession))
}
// GetColorFromSession is function-type
func GetColorFromSession(intent alexa.RequestIntent, session alexa.Session) alexa.Response {
cardTitle := intent.Name
sessionAttributes := make(map[string]interface{})
shouldEndSession := false
speechOutput := ""
repromptText := ""
if favoriteColor, ok := session.Attributes["favoriteColor"].(string); ok {
speechOutput = "Your favorite color is " + favoriteColor + ". Goodbye."
shouldEndSession = true
} else {
speechOutput = "I'm not sure what your favorite color is, you can say, my favorite color is red"
}
return alexa.BuildResponse(sessionAttributes, alexa.BuildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession))
}
// GetNoEntityResponse is function-type
func GetNoEntityResponse() alexa.Response {
cardTitle := ""
sessionAttributes := make(map[string]interface{})
shouldEndSession := false
speechOutput := ""
repromptText := ""
return alexa.BuildResponse(sessionAttributes, alexa.BuildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession))
}
/*
* Events
*/
// OnSessionStarted is function-type
func OnSessionStarted(sessionStartedRequest map[string]string, session alexa.Session) (alexa.Response, error) {
fmt.Println("OnSessionStarted requestId=" + sessionStartedRequest["requestId"] + ", sessionId=" + session.SessionID)
return GetNoEntityResponse(), nil
}
// OnLaunch is function-type
func OnLaunch(launchRequest alexa.RequestDetail, session alexa.Session) (alexa.Response, error) {
fmt.Println("OnLaunch requestId=" + launchRequest.RequestID + ", sessionId=" + session.SessionID)
return GetWelcomeResponse(), nil
}
// OnIntent is function-type
func OnIntent(intentRequest alexa.RequestDetail, session alexa.Session) (alexa.Response, error) {
fmt.Println("OnLaunch requestId=" + intentRequest.RequestID + ", sessionId=" + session.SessionID)
intent := intentRequest.Intent
intentName := intentRequest.Intent.Name
if intentName == "MyColorIsIntent" {
return SetColorInSession(intent, session), nil
} else if intentName == "WhatsMyColorIntent" {
return GetColorFromSession(intent, session), nil
} else if intentName == "AMAZON.HelpIntent" {
return GetWelcomeResponse(), nil
} else if intentName == "AMAZON.StopIntent" || intentName == "AMAZON.CancelIntent" {
return HandleSessionEndRequest(), nil
}
return alexa.Response{}, ErrInvalidIntent
}
// OnSessionEnded is function-type
func OnSessionEnded(sessionEndedRequest alexa.RequestDetail, session alexa.Session) (alexa.Response, error) {
fmt.Println("OnSessionEnded requestId=" + sessionEndedRequest.RequestID + ", sessionId=" + session.SessionID)
return GetNoEntityResponse(), nil
}
// Handler is main
func Handler(event alexa.Request) (alexa.Response, error) {
fmt.Println("event.session.application.applicationId=" + event.Session.Application.ApplicationID)
eventRequestType := event.Request.Type
if event.Session.New {
return OnSessionStarted(map[string]string{"requestId": event.Request.RequestID}, event.Session)
} else if eventRequestType == "LaunchRequest" {
return OnLaunch(event.Request, event.Session)
} else if eventRequestType == "IntentRequest" {
return OnIntent(event.Request, event.Session)
} else if eventRequestType == "SessionEndedRequest" {
return OnSessionEnded(event.Request, event.Session)
}
return alexa.Response{}, ErrInvalidIntent
}
func main() {
lambda.Start(Handler)
}
やはり、実際の作業ではalexa/request.go
、alexa/response.go
とmain.go
の間で行き来しながら、書きました。ただ、VSCodeに入れていたLintツールがコンパイルレベルのチェックはかけてくれていたので、実際にコンパイルやデプロイを経る途中確認はあまり行っていません。
ここまでの作業を終えると、次のようなディレクトリ・ファイル構造になっていると思います。
alexa-skills-kit-color-expert-go
┣ alexa
┃ ┣ request.go
┃ ┗ response.go
┣ main.go
┣ serverless.yml
┗ README.md
コンパイルとデプロイ
Lambda(Linux)の実行ファイルにコンパイルします。
$ GOOS=linux go build -o bin/main
ここで、試しにローカル実行してみたのですが、Goは未対応でした。
$ sls invoke local --function ask-go --path event.json
Serverless Error ---------------------------------------
You can only invoke Node.js, Python & Java functions locally.
Get Support --------------------------------------------
Docs: docs.serverless.com
Bugs: github.com/serverless/serverless/issues
Forums: forum.serverless.com
Chat: gitter.im/serverless/serverless
Your Environment Information -----------------------------
OS: darwin
Node Version: 6.10.0
Serverless Version: 1.26.0
ですので、このままAWSにデプロイします。
$ sls deploy
AWSにデプロイできたら、出来上がったLambda関数のテストを行っておきましょう。Alexa Intent - MyColors
を使います。
テストの実行結果がこんな具合に返ってくればOKです。
そして、出来上がったLambda関数のARN
(arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:ask-kintone-dev-ask-go
)をメモっておきます。
Alexaスキルの設定
developer.amazon.com のコンソールから、Alexaスキルの設定を行って完成です。設定はチュートリアルのSTEP2からの手順を進めていきます。メモっておいたARN
はSTEP2-14の設定で使います。
確認
無事全ての設定を終えたら、実機やEchosim.io等で、サンプル通りに設定できているか確認します。
所感とまとめ
Go/Lambda
- 十数年振りの静的型付けがある言語はちょっとしんどかったが、新しい学びがあって楽しかった
- Goの強みとされる速さ、軽さ、並行処理といった部分が生きてくるケースではLambda前提で実際に使ってみたい(ただ、まぁまぁな学習コストになりそう)
- 逆にGoの強みが顕在化しない、必要ないケースでは、自分は書き慣れたNode.jsやPythonを選択すると思う
-
event
オブジェクトとreturn
するオブジェクトは、interface{}
にしておけば、JSONと構造体の突き合わせは不要だったかもしれないと思った(サンプルでは定義されていたので、同じようにやってみたものの) - go-kintoneをLambdaで試してみたい
Alexa
- 久々にAlexaスキルのロジック書いて、理解が深まった
- スキルビルダーで
インテント
やスロット
、サンプル発話
が設定しやすくなっていた - Alexa開発のための言語選択という点では、Node.jsについては、SDK(Alexa Skills Kit SDK for Node.js)の登場で非常に楽に書けるようになってきたので、あえてGoを使うというケースはレアケースになるように思う。作りたいスキルとそれを実現するための開発ライブラリの充実に寄るところもあると思うが(Lambdaを使わずにAlexaのサーバーサイドロジックを構築するというのであれば・・・いやそれでもどうだろう)
免責
こちらは個人の意見で、所属する企業や団体は関係ありません。
参考にした情報
- Announcing Go Support for AWS Lambda
- Serverless Framework example for Golang and Lambda
- Programming Model for Authoring Lambda Functions in Go
- Alexa Skills Kitによるスキルの作成
- カスタムスキルのJSONインターフェースのリファレンス
- カスタムスキルのAWS Lambda関数を作成する
- 標準のリクエストタイプのリファレンス
- Alexaから送信されたリクエストを処理する
- https://github.com/alexa
- Alexaスキル開発トレーニング
- Go言語の初心者が見ると幸せになれる場所 #golang
- はじめてのGo―シンプルな言語仕様,型システム,並行処理