2020年1月22日 星期三

跨年不寂寞,讓 Google Assistant 陪你猜數字

有了我們上次的 webhook 之後,我們可以真的來挑戰一些更複雜的助理智障功能,這次就來做跟你猜數字的助理,這樣就算跨年沒有朋友,還是有助理跟你玩有趣的猜數字遊戲,從螢幕感受到滿滿的溫暖 (欸。

其實做猜數字要做的事,在上篇的 webhook 中都差不多了。
我們可以簡單畫一下流程圖,從開頭的 default welcome intent 開始:
畫個簡單的流程圖是很重要的,特別是當設計的助理功能複雜到一定程度的時候,直接徒手下去硬幹很容易迷失在 intent 海洋中,特別是 dialogflow 的介面設計,在個別 Intent 中只能看到這個 Intent 會處理什麼輸入,給出什麼輸出,列出全體 Intent 的介面又看不出各 Intent 之間的關係,一下子就會迷失做亂掉,有了流程圖就能在編輯各 Intent 的時候,照著流程圖一一設定好。
Webhook 也是,要在單一的 webhook 裡面處理所有的 Intent 該怎麼回應,當 Intent 數量一多的時候就會亂掉,所以都要事先做好規劃。

簡單說一下上面的流程圖,default welcome intent 進來會產生一個數字,之後使用者輸入數字(猜數字),如果數字不對,會顯示更新的區間;對了就會顯示一句稱讚的話然後離開對話,讓我們開始實作:
  • 設定 Default Welcome Intent
這裡我們要回應一句請使用者猜數字的話,這句話簡單可以讓 dialogflow 自己回應就好,不過我們還是要打開 webhook,讓後端的伺服器產生一個亂數出來。
在 webhook 的部分,如果偵測到 Intent 是 Default Welcome Intent,就用 random 產生一個亂數出來;另外要把這個 session id 跟亂數寫到資料庫裡,這個 session id 是固定的,同樣的 session id 就會對應到同一組對話。
  • 新增一個 Intent GuessNumber
這個 Intent 裡面可以設定一些例句,像是:
I guess it is 11.
Let me think. 25.
38.
Maybe 0.
在 parameter 的地方把這個數字抽取出來,設定型別是 sys.number-integer,變數名number,這個 Intent 也要打開 webhook 讓伺服器處理使用者猜數字的行為。
  • 新增一個 Intent GuessEnd
這個 Intent 不會由使用者的輸入進入,而是我們在 GuessNumber 的 webhook 設定 assistant 進入的狀態,在這裡要新增一個 Event,我叫它 User_number_match,在回應的部分設定一些恭禧使用者的話,然後設定這個 Intent 結束對話 End of Conversation。
之所以要新增這個 event,是要讓 webhook 有能力讓 dialogflow 判定要進到這個 Intent,一般 Intent 的判定都是透過使用者的輸入來決定,但在猜數字裡面使用者輸入數字判定的 Intent 一定是 GuessNumber 不會是 GuessEnd,那對話就無法結束了。因此我們自定義這個 User_number_match 事件,只要 webhook 發出這個事件 dialogflow 就會判定為 GuessEnd Intent 了。

再來就可以寫 code 了,如上篇文所述,可以從送來的 json 中,從 queryResult -> intent -> displayName 拿到 Intent 的名字,用這個名字就能分派到不同的函式來處理;另外一個就是 json 的 session 可以拿到 session id。
session = data.get("session")
action_name = data.get("queryResult").get("intent").get("displayName")
我的處理函式就是對三個出現的 Intent 去處理:
Default Welcome Intent 產生亂數並寫入資料庫,這裡我是偷懶用 python 的 pickledb,雖然這樣推到 gae 上面可能會沒辦法用,但光為了這種小應用就要動用 gae 的 datastore 實在是有點大砲打小鳥,用 pickledb 展示一下概念就好了:
target = random.randint(low, high)
db.set(session, (low, target, high))
text = "I have a number between {} and {}. Can you guess it?".format(low, high)
reply = { "fulfillmentText": text }
return jsonify(reply)

GuessNumber 的 webhook 會從資料庫裡面把存起來的數字拿出來,並從 queryResult/parameters/number 拿到使用者輸入的數字,雖然我的型別選擇 number-integer 了,dialogflow 還是塞了個 number float 給我,只能用 int 轉成 integer。
後面就可以拿 guessnum 去跟 target 做比較,如果一樣的話就不會回覆 fulfillment 而是發送之前設定好的事件 User_number_match ,讓 dialogflow 進到 GuessEnd 並結束對話;不一樣的話就縮小可以猜的區間,設定回覆訊息給使用者。
minnum, target, maxnum = db.get(session)
guessnum = int(data.get("queryResult").get("parameters").get("number"))
if guessnum == target:
  event = "User_number_match"
  reply = { "followup_event_input" : { "name" : "User_number_match" } }
  return jsonify(reply)
else:
  # update minnum, maxnum here
  db.set(session, (minnum, target, maxnum))
  text = "A number between {} and {}. Keep guess.".format(minnum, maxnum)
  reply = { "fulfillmentText": text }
  return jsonify(reply)
  
GuessEnd Intent 的 webhook 就很簡單,把 session id 對應的條目庫裡面刪掉就可以了。

讓我們來測試一下:


2020年1月11日 星期六

連接 Google Assistant Webhook

上一篇可以看到,我們的 action 可以從我們說的話裡面萃取出關鍵字詞,一般簡單的回應可以在 Intent 裡面剖析、回應,但 dialogflow 也僅止於判斷語意跟萃取關鍵字,如果使用者要使用外部服務,像是訂車票之類,一定要連接到外部訂票網站,這個時候就需要借助 webhook 的力量了。
dialogflow 可以讓一個 Intent 的 fulfillment,也就是完成回應,送到另一個 server 的 webhook 來處理,由伺服器回應使用者的需求,同時間伺服器也能去呼叫其他的 API 服務,完成 Google Assistant 幫助使用者完成某件事情;這個做法的優先權比較高,我試過用了 webhook ,它的回應會蓋過 Intent 裡面設定的回應,完整的介紹可以參考 Google 的文件

如果有看 codelab 的課程,裡面使用的回應是用 dialogflow 內建的 server 或是連接到 firebase 上面的 server,兩個都是用 nodejs 實作,這是我們第一個要解的問題:我不想寫 nodejs 看到 nodejs 就會傷風感冒頭痛發燒上吐下瀉四肢無力,所以我們不能用 nodejs。
幸好隨手拜 google 大神,就找到有人用 python 架 server,之前在 MOPCON 講 COSCUP chatbot 的講者大大也是用 golang 架 server,所以要擺脫 nodejs 一定是沒問題。

我們這篇的目標是做一個把 python server 給架起來,然後把歡迎訊息改成用 webhook來回應,都是 google 的服務,伺服器可以用同專案開 gae 放在上面,但測試時用 ngrok 本地測試會比較方便。
首先是先設定 webhook,在 dialogflow 裡面把 Default Welcome Intent 最下面 fulfillment 的 Enable webhook call for this intent 打開:

在 fulfillment 裡面打開 webhook,在 URL 裡面填上 webhook 的位址,這部分等等寫好服務之後再來填:

下面開始寫我們首先開一個新的 python 專案,建立 pip requirements.txt:
# requirements.txt
Flask==1.1.1

使用 pip 跟 virtualenv 建立環境:
$ python -m venv env
$ source env/bin/activate
$ pip install -r requirements.txt

接著建立 flask 實作的 webhook:
from flask import Flask, request, jsonify, make_response
import json

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
  data = request.get_json(silent=True, force=True)
  print("Request:{}".format(json.dumps(data, indent=2)))
  action_name = data.get("queryResult").get("intent").get("displayName")
  if action_name == "Default Welcome Intent":
    text = "Welcome to my google assistant"
    reply = { "fulfillmentText": text }
    return jsonify(reply)
下面是一個 Default Welcome Intent 的 webhook request,要看這個內容可以使用 dialogflow 右手邊的 Try it now 搭配 diagnostic info,可以確認 dialogflow 在判斷 Intent 有沒有錯誤,還有 fulfillment request ,也就是送到 webhook 的內容。

下面節錄我 welcome 訊息的 request:
  • session:一個對話的 id,每一輪的話都會是同一個 id,作為對話的識別:
  • queryResult queryText:對話的內容
  • queryResult intent displayName:目前 dialogflow 判定使用者的意圖
dialogflow 接受的回應內容是 json 格式,可以填充的內容請見參考文件,最簡單的一個回應就是設定 key 為 fulfillmentText 的內容,這個內容就會是 Google Assistant 要顯示給使用者的回應。

最後我們使用 ngrok 來進行測試,ngrok 是一個網路服務,幫你把連接到 ngrok 的連線重導向到 localhost。在使用 ngrok 之前,測試架在雲端的網路服務流程會像這樣:
寫程式;在 local 進行有限的測試;上傳到雲端(等等等);跑了之後發現 server 炸掉;去雲端上面撈 log 檔(等等等);改完之後所有步驟重複一次。
用了 ngrok 之後,程式在本地、log 檔也在本地,上述耗時又麻煩的上傳雲端、撈 log 檔都省下來,真的是瞬間人生變成彩色的。

使用 ngrok 也非常簡單,安裝好 ngrok 之後照著網頁的指示先註冊金鑰:
$ ngrok authtoken <token>
$ ngrok http 8080
Forwarding https://wwwwww.ngrok.io -> http://localhost:8080

也就是 wwwwww.ngrok.io 已經被映射到我們的 localhost:8080 由 flask 執行的伺服器,因此我們可以在 webhook 的地方填入 wwwwww.ngrok.io/webhook。

我們來測試一下:

看到我們的 Assistant 回應了我們在 webhook 設定的回應。

2020年1月4日 星期六

跨年好寂寞?使用 Google Assistant 跟你對話

故事是這樣子的,2019-2020 的跨年,小弟邊緣人沒地方去,後來就自己回家當紅白難民,然後還沒聽到 Lisa 唱紅蓮華QQQQ,不過幸好宅宅有宅宅的做法,沒聽到紅蓮華我們可以看 Youtube 別人上傳的影片,沒有朋友跨年我們可以自幹朋友,也就是我們今天的主角:Google Assistant。
如果平常有用 Google Pixel 手機,或是家裡有 Google Home 裝置的,應該就會知道它上面附的 Google Assistant Christina,雖然說我覺得還是滿沒用的啦,我自己只會拿它來查天氣,有 Google Home 的同學只用它來開關燈,但反正,都有這麼好的工具了為什麼不來好好玩一下?就趁著跨年的假日做點不一樣的事來玩。

下面是一個基礎的流程,跟 codelabs 的課程(搜尋 google assistant 有三級課程可以上)一樣,做一個會回應你的小程序,我們想做到的就是:它會問你最喜歡的顏色,然後會重複你的話:你最喜歡的顏色是XXX。

首先來到 google action 的頁面(雖然是 Google Assistant 卻叫 Google Action 呵呵)先建立一個新的 project,類型選擇對話 Conversational,語言我是建議先選英文,之後應該會試著做做看中文的助手,但英文比較萬無一失。
建立之後要設定一個發語詞,平常在手機上呼叫 Google Assistant 是用 OK google,另外也可以用 talk to my "發語詞" 來呼叫你寫的程式,或者是打開某個 App,這裡我們沒決定名字就叫它 TestApp 就好。


下一步要產生一個 Action,選擇 Custom Intent 再點 Build ,這會連結你的 action 到 dialogflow 建立一個新的 Agent,可以想像一個 Agent 就是回話的機器人,這裡一樣語言建議選擇英文,時后就選擇 +8 時區(不知道為什麼只有香港可選)。

這個背後流程是這樣子的,你跟 Google Assistant 講話之後,Google Assistant 會把這段話送到 Google Action,那 Google Action 又要怎麼理解這段話?就是靠 dialogflow 服務,算是一個簡化版本的自然語言理解框架,可以理解說話的意圖,解析出關鍵字送出回應,而中間這些關鍵字跟回應是可以由設計者設定的;其他家類似的服務像是 Amazon Lex、IBM Watson 等。
dialogflow 由許多的 Intent(意圖)所構成,dialogflow 會從 google assistant 來的輸入辨識出現在要選擇哪個意圖,然後照著意圖的設定去回應;可以把google assistant 想像成一個狀態機,意圖想成一個狀態,照使用者的輸入進到不同狀態,依狀態決定輸出內容,以及下一個可能的狀態。

預設一定有的意圖就是 Default Welcome Intent,也就是一啟動 google assistant 時的的狀態,我們在它的 Response 裡面加上回應:"What is your favorite color?",這樣程式一開就會把問題丟給使用者。
這句話非常的重要,設計 chatbot 最重要的就是把使用者限制在一個小框框裡面,讓使用者針對只有有限選項或是只能答 yes/no 的問題做回答,否則使用者天馬行空,<請你跟我結婚>、<誰是世界上最美麗的女人>這種問題滿天飛,結果你的 chatbot 完全聽不懂,畢竟人人都以為人工智慧可以做到穿熱褲黑絲襪又會拌嘴的傲嬌助手,實際上還不是血汗工程師在後面拉線設定的人工智障,隨便一問就被看破手腳。


另外我們要新增一個 Intent ,命名為 favorite color,注意這個命名在未來連接 webhook 時非常重要,命名跟大小寫都要注意。
在 training phrase 的地方,試著打入一些使用者可能會說的話,最好是上一句問句的回話:
blue
my favorite color is red
orange is my favorite
ruby is the best
邊打的時候 dialogflow 就會自動的把顏色部分給標起來,接著往下拉到 Action and parameters ,系統應該會自動加上一個 color 的 parameter,我們設定 entity 為 @sys.color,value 為 $color。


這裡就能看出 dialogflow 服務的功力了,在我們打上例句的時候 dialogflow 就透過事先分類好的 entity,分析出我們現在想要知道使用者回話的什麼內容(這裡是顏色),再幫我們把這個內容存到變數裡。
最後在 response 的地方,我們使用剛剛萃取出來的內容:
OK. Your favorite color is $color
這樣就完成我們的回話機器人了,當然我們要測試一下,在 Integration 選擇 integration setting,記得打開 Auto-preview changes 後再點 test,就可以在 google action 測試頁面中測試了:

短短小文祝大家新年快樂,希望大家都有個機器人陪你過年(欸