2017/6/14

讓 Alexa 記得你 - Alexa Skill 的變數存取


所謂萬事起頭難, 當我們從頭到尾摸索一遍Alexa Skill的整個專案開發過程之後, 我們將發現原來「Hello World」是最難的! 而在熟悉開發環境與流程之後, 接下來天馬行空的應用都變得很簡單, 我們只須專注在程式設計的本業部份.


Alexa Skill的變數型別
本案例希望Alexa語音助理可以透過詢問使用者名字的方式並將得到的名字資料(字串)儲存起來, 直到使用者反問時再將名字資料萃取出來並透過Echo回答說出. 因此, 透過本案例我們將知道Alexa Skill的資料定義與存取方法.

依照官網的線上說明, Alexa Skill內建的變數型別如下, 但似乎沒有我們要的簡單字串資料型別(以儲存名字). 因此我們必須透過「Custom Slot Types」自己建立一個.





增加自訂的變數型別
Alexa Skill Kit開發網頁Interaction Model步驟, 除了於前例「開始寫第一個 Alexa Skill – Hello World!」所提到開發者須定義的「Intent Schema」與「Sample Utternaces」之外, 我們可以發現還有一項之前略過的「Custom Slot Type」定義. (其它細項操作/步驟請參考的說明)

其實我們想要的只是簡單的字串型別(以儲存名字), 但目前似乎只能先透過這個方式達到目的. 按下「Add Slot Type」之後就可以編輯我們自訂的資料型別, 此時系統需要我們填入一些初始值, 我們隨便填入/列舉一些值給它即可(當然我們將來也可檢查資料是否在定義/列舉的集合之中). 舉例來說, 我們取一個稱LIST_OF_NAME的資料型別, 並預設有A, BC三個成員. 讀者也可以自己取一個稱STRING的資料型別, 並只留一個如string之類的預設值給它. 這個新增的資料型別, 我們將在定義「Intent Schema」時用上.





變數值的定義方式
還記得前案曾說明Alexa Skill的運作流程? ASK會先將使用者語音輸入全部辨識成文字, 接著指認出「Invocation(要被激活的App名稱)」與「Intent(剩餘的文字指令)」並將所有資料以JSON打包成Lambda Request遞送到AWS. 因此, 初學者若想知道如何定義包含使用者自訂變數的「Intent Schema, 可以試著先從ASK模擬器將資料打包成Lambda Request的內容中嗅出一些端倪.

舉例來說, 若我們希望使用者透過說出「My name is {myName}」時來處發「MyNameIsIntent,同時將{myName}這個變數值存起來. 另一方面, 希望當使用者說出「What’s my name」時來處發「WhatIsMyNameIntent」並且將變數值取出/說出.



從上圖模擬器的Lambda Request監視窗中, 我們可以清楚看到, 我們所定義的「Intent」是包含自訂變數{myName}, 並以稱為「slot」的結構來存放. 請參考上圖右上方區塊的「Intent Schema」規則, 本例為包含變數的MyNameIsIntent」定義方式, 其中定義了{myName}的變數與其資料型別. ASK在成功截取使用者名字語音之後, 會自動將値填入slots.myNamevalue欄位.



變數值的存取方式
在知道ASK如何成功觸發「Intent」並合併使用者名字以JSON(Lambda Request)遞送至AWS之後, 接下來我們須進一步了解如何在Lambda的程式中取出它. 如下圖, 因為intent是以JSON物件形式當參數被傳遞到我們的Intent Handler, 因此我們可以非常容易地以物件名稱的方式取值, 本例為intent.slots.myName.value.

Lambda函式之間的參數傳遞也是透過稱「sessionAttributes」的JSON物件形式交換, 如下圖, 我們從intentJSON物件中將値取出後透過系統預設的回呼函式將該變數打包. 我們可以在模擬器的Lambda Response監控窗中看到它, 如本例中系統將區域變數Name透過回呼函式將該變數與值打包在sessionAttributes裏面.




透過下面這段測試程式, 讀者將可以更清楚地猜出回呼函式在底層對sessionAttributes的資料封裝與其取用的方式. 本例中, 我們在setAttributeInSession中設定兩個區域變數(含值)並指定給sessionAttributes, 最後在getAttributeInSession中透過sessionAttributes將值取回.






程式架構
請參照案之Alexa語音服務的運作流程圖, Alexa Skill程式的撰寫與其開發環境/架構目前分成兩個區塊協同運作, 也就是由兩個不同的網頁/服務所構成. 因此在開始為Alexa開發新的技能專案前, 建議我們心裡能先有一個藍圖來規劃需要的「Intent Schema(包含自訂變數)、「Sample Utterances」與被觸發之後的「Intent Handler, 如下圖所示.





左半部描述在的Alexa Skill Kit (ASK) 開發者平台, 我們將在此為Alexa定義新的技能. 當使用者「調用(Invoke)」自己寫的App程式「myName, 緊接著當我們說出的語音指令符合所預先例舉的樣本(Sample Utterances)並觸發所定義的「意圖(Intent), 本例為「MyNameIsIntent」與「WhatIsMyNameIntent.

接著ASK將資料(包括自訂變數與值)打包成JSON提出AWS/Lambda請求, 而右半部則是描述在AWS/Lambda平台將針對各個「Intent提供底層服務(Intent Handler)的實作.



Alexa記得並說出使用者名字的Skill

Step1: Skill Information
ASK網頁新增一個叫做「my Name」的Skill, 其它細節請參考前例「開始寫第一個 Alexa Skill – Hello World!.


Step2: Interaction Mode

ASK/Intent Schema
意圖模式(Intent Schema)是以JSON的格式來定義. 如下, 我們為新的Skill添增一個稱為「MyNameIsIntent」的意圖, 包含自訂資料型態的變數{myName}, 用來詢問/儲存使用者名字, 以及一個「WhatISMyName」的意圖, 用來取出/回應使用者名字並透過Echo發聲.

{
  "intents": [
    {
      "slots": [
        {
          "name": "myName",
          "type": "LIST_OF_NAME"
        }
      ],
      "intent": "MyNameIsIntent"
    },
    {
      "intent": "WhatsMyNameIntent"
    },
  ]
}



ASK/Sample utterances
為了讓Alexa能成功辨識使用者語音指令並觸發我們所定義的「Intent, 因此我們須先預想幾種可能的使用者招喚方式, 例如:

MyNameIsIntent my name is {myName}
WhatsMyNameIntent what's my name



Step3: 撰寫Lambda程式
這個步驟須將焦點暫時從ASK網頁移到AWS/Lambda網頁, 為我們設定的「Intent」實作底層的處理函式(Handeler). 我們以前案「開始寫第一個 Alexa Skill – Hello World!」的程式為藍圖/範本當開發者的起點, 因此只須改寫下面三個函式. 資料的存取與傳遞方式請參考前面的說明.

// Called when the user specifies an intent for this skill.
function onIntent(intentRequest, session, callback) {
    console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`);

    const intent = intentRequest.intent;
    const intentName = intentRequest.intent.name;

    // Dispatch to your skill's intent handlers
    if (intentName === 'MyNameIsIntent') {
        setNameInSession(intent, session, callback);
    } else if (intentName === 'WhatsMyNameIntent') {
        getNameFromSession(intent, session, callback);
    } else {
        throw new Error('Invalid intent');
    }
}

// MyNameIsIntent Handler
function setNameInSession(intent, session, callback) {
    var sessionAttributes = {};
    var cardTitle = intent.name;
    var nameSlot = intent.slots.myName; // slot of MyNameIsIntent in JSON
    var speechOutput = "I'm not sure what your name is. Please try again.";
    var repromptText = "";
    var shouldEndSession = false;

    if (nameSlot) {
        var Name = nameSlot.value;
        sessionAttributes = {Name}; // return custom variable in JSON
        speechOutput = Name + ". I now know your name is " + Name +
            ". You can ask me your name by saying, what's my name?";
    }
    repromptText = speechOutput;
   
    callback(sessionAttributes,
         buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

// WhatIsMyNameIntent Handler
function getNameFromSession(intent, session, callback) {
    var sessionAttributes = {};
    var speechOutput = "I'm not sure what your name is.";
    var repromptText = "";
    var shouldEndSession = false;

    if (session.attributes) {
        var Name = session.attributes.Name; // fetch variable from sessionAttributes (JSON)
        if (Name) {
            speechOutput = `Your name is ${Name}. How are you?`;
            shouldEndSession = true;
        }
    }
    repromptText = speechOutput;

    callback(sessionAttributes,
         buildSpeechletResponse(intent.name, speechOutput, repromptText, shouldEndSession));
}



按儲存之後請將網頁編輯器最右上方的ARN碼複製下來, 它將用來綁定ASK的外部服務service endpoint, 它的格式為: arn:aws:lambda:us-east-1:xxxx:function:myName.


Step4: Configuration
完成Lambda程式並取得ARN碼之後我們須回到ASK的頁面繼續完成configuration, 如下圖, service enpoint中勾選AWS Lambda並填入ARN code.


Step5: 測試
ASK提供了文字轉語音輸出的模擬器與Echo語音助理的模擬器, 因此即使我們手邊並沒有真正的Echo硬體設備也能模擬真實的結果. 從模擬器中可以清楚看見底層資料以JSON的遞送/交換過程, 當使用者說出「my name is …, ASK從列舉的utternaces中成功辨識/匹配的「MyNameIsIntent」並將自訂的變數與值打包成JSON交由Lambda處理. 之後setNameInSession函式(Intent Handler), 把變數與值存放在「sessionAttributes. 接著當使用者詢問「what is my name」時, 再次符合列舉的utternaces發出Lambda請求並觸發「WhatIsMyNameIntent, 最後透過getNameInSession函式把變數與值從「sessionAttributes」取回之後再以JSON傳回ASK接手後續的Echo發聲與手機文字顯示等服務.








測試步驟:
筆者: Alexa, ask my Name     (調用自己寫的App)

(: 本例因沒有任何接續的語音指令Intent可供與sample utterances匹配, 系統會觸發預設的AMAZON.HelpIntent, 其對應的HandlergetWelcomeResponse函式, 有興趣的讀者可以自行修改該函式當作Skill使用說明)

筆者: my name is Hock.
筆者: what’s my name?