Lesson 10: Chatbot with DialogFlow
last updated: 2022/06/08
以DialogFlow為引擎
Part1: 登入DialogFlow 建立 Agent(代理人)
Part2: 建立App與代理人的中介橋樑(webhook)
Part 3: 以Ionic建立Chatbot App
Part 3. Ionic App
Part 2. Webhook
Part 1. Agent
Webhook: 為一網址(REST API,必須架設公開網站)
Create Agent
輸入代理人名稱
設定預設語言別
綁定Google專案
測試主控台
建立新的意圖
預設Intents
已產生兩個Intents: Fallback, Welcome
輸入測試語句
何時觸發Welcome? 偵測到「開始對話」的語句
何時觸發Fallback? 偵測到agent不了解的語句
我要預約
明天天氣預報
你們店怎麼去?
使用者字句
可能的意圖
預約
查詢天氣
查詢地址
2-1-1 客製化Welcome意圖:需符合系統目標、Bot的人設
❸存檔
❶ 刪除預設回應訊息
❷ 新增回應訊息如上
歡迎光臨!你想知道本店營業時間,還是想要預約來店?
❶ 多加幾種不同的歡迎詞
2-1-2 建立回應訊息的變形
❷ 存檔後測試
你好!我可以告訴你本店營業時間,也可以幫你預約,你需要什麼服務呢?
嗨!有什麼需要幫忙的嗎?我可以告訴你本店營業時間,也能幫你預約來店。
❶ 建立新的意圖
2-2-1 兩個工作: 告知營業時間、預約 ➔ 自訂意圖
❶ 輸入意圖名稱
2-2-1 兩個工作: 告知營業時間、預約 ➔ 自訂意圖
❷ 存檔
❸ 新增訓練用片語
❶ 輸入多個回應訊息
2-2-3 建立回應訊息
❷ 存檔
2-3 不了解使用者字句時,觸發Fallback意圖
❸存檔
❶ 刪除預設回應訊息
❷ 新增回應訊息如上
抱歉,請問你是要詢問營業時間?還是要預約來店呢?
不好意思,請問你是想知道營業時間?還是要預約呢?
「預約」意圖必須能夠處理更複雜的句子
❶ 辨識出3 PM, today為重要資訊
❷ 回應訊息附上剛提及的日期、時間資訊
Dialogflow要能辨識重要資訊、並將之儲存
參數(parameters), 每個辨識出來的參數都有自己的實體型別(entity type)
2-4-1 新增「預約」意圖
❶ 輸入我要預約今天下午三點
❷ 系統自動偵測出兩個參數
或
手動標示兩個參數並選擇正確的entity type
❸存檔
2-4-1 新增「預約」意圖(續)
❶ 回應訊息輸入上列文字
❷存檔
收到!為你預約 $date 時間是 $time 來店賞車,期待你的光臨。
2-4-2 加入更多的訓練用片語
❶ 加入更多訓練用片語
❷存檔
我想看看最新款的單車
包含一些不含日期時間的句子
我的車壞了
2-4-3 設定「參數」為必要資訊 (slot filling)
❶ 勾選為「必要」REQUIRED參數
我想看看最新款的單車
❷ 打開Define prompts,輸入後續提示字句
我的車壞了
使用者輸入的句子經常不包含日期、時間,要如何預約
❸存檔
2-4-4 填入「參數」缺少時的提示訊息 (slot filling)
❶ 加入後續提示字句
以缺少日期為例,輸入後續Bot應該回應的問句
❷存檔
2-4-4 填入「參數」缺少時的提示訊息 (slot filling)
❶ 加入後續提示字句
缺少「時間」時,後續Bot應該回應的問句
❷存檔
預約若要記錄下來,需要外部程式(透過webhook)協助完成
Fulfillment運作示意圖
Fulfillment: 用來解決使用者要求的「外部服務」
(用量超過一定數量需付費)
目標:Dialogflow收到「日期」、「時間」,記錄到Google日曆
❶ 開啟wehook call選項
❷存檔
前往「預約」intent
webhook外部服務:使用內建編輯器inline editor, 支援node.js程式
❶ 開啟inline editor選項
fulfillments
❷ 在此處寫程式
❸ 部署
Firebase cloud function
Heroku
...
部署支援https協定的公開網站
1. 紅色為user輸入 藍色為機器人回覆
smalltalk.greetings.hello
smalltalk.user.wants_to_talk
smalltalk.agent.good
2. 在DialogFlow設定了上述三個意圖
(intents)
# 建立空白專案
ionic start ChatbotApp blank
cd ChatbotApp
# 安裝DialogFlow SDK
npm install @google-cloud/dialogflow
# 建立chat service
ionic g service services/chat
自然語言理解套件:google cloud dialogflow
Ⓐ 新增model資料夾
新增「資料模型.ts檔」
Ⓒ 建立服務
ionic g service 服務名稱
實作資料存取
前往DialogFlow網站,以google帳號登入
建立agent(以Prebuilt Agents的Small Talk為原型)
選擇IMPORT
V2 API,並找到Client access token
○
⦿
修改src/environments/environment.ts檔
貼上client access token備用
export const environment = {
production: false,
firebaseConfig: {
apiKey: ...
...
},
dialogflow: {
angularBot: 'client access token貼於此'
}
};
environments/environment.ts
export interface Message {
userId: string; // user id
name: string; // 發訊息的使用者暱稱
message: string; // 訊息內容
timestamp: Date; // 發訊息的時間標記
}
services/chat.service.ts
import { environment } from '../../../environments/environment';
import { ApiAiClient } from 'api-ai-javascript/es6/ApiAiClient';
import { BehaviorSubject } from 'rxjs';
import { Message } from './../../model/message';
...[略]...
❶ 引入所需元件
model/message.ts
Ⓐ 新增資料模型
Ⓒ 實作服務
...[略]...
export class ChatService {
// 使用Dialogflow agent提供的access token,連往AI引擎
readonly token = environment.dialogflow.angularBot;
readonly client = new ApiAiClient({
accessToken: this.token
});
...[略]...
❷ 建立 Dialogflow (ApiAiClient) client端物件
// 可供訂閱的對話主題(出版端)
conversation = new BehaviorSubject<Message[]>([]);
constructor() { }
...[略]...
}
❸ 建立 對話主題 (可供訂閱的Message物件陣列)
services/chat.service.ts
services/chat.service.ts
A.輸入問題
ApiAiClient
自然語言處理
m1 | m2 | m3 | m4 | ... | mx | mx+1 | ... |
---|
訂閱
訂閱
BehaviorSubject資料流
1.填入資料流
3.透過ApiAiClient物件向DialogFlow要求服務
4.處理結果填入資料流
B.訂閱資料流
2.5.資料更新
通知
顯示
chat.service.ts: (1) 提供method: 完成步驟1~4 (2) 提供資料流供訂閱
HomePage: (A) 輸入問題 (B) 訂閱、顯示
資料流
export class ChatService {
...[略]...
converse(userMsg: Message) {
this.update(userMsg);
return this.client.textRequest(userMsg.message)
.then( (res) => {
const speech = res.result.fulfillment.speech;
const botMsg: Message = {
userId: 'bot',
message: speech,
name: 'DialogFlow',
timestamp: new Date()
};
this.update(botMsg);
});
}
update(msg: Message) {
this.conversation.next([msg]);
}
}
5. 建立 Dialogflow (ApiAiClient) client端物件
❷ next(): 已訂閱者會收到此新增資料
❶ 問題放入資料流
userMsg內含使用者輸入問題
建立機器人回應的訊息結構
❸ textRequest(msg): 丟到ApiAi
❹ ApiAi 回傳結果
❺ 回覆資料放入資料流
services/chat.service.ts
export interface IServerResponse {
id?: string;
result?: {
action: string,
resolvedQuery: string,
speech: string;
fulfillment?: {
speech: string
}
};
status: {
code: number,
errorDetails?: string,
errorID?: string,
errorType: string
};
}
ApiAi回覆訊息的結構:供參考
'fulfillment': 執行回覆
export class ChatService {
...[略]...
.then( (res) => {
const speech = res.result.fulfillment.speech;
...[略]...
}
import { Message } from './../../model/message';
import { Injectable } from '@angular/core';
import { ApiAiClient } from 'api-ai-javascript/es6/ApiAiClient';
import { environment } from '../../../environments/environment';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ChatService {
readonly token = environment.dialogflow.angularBot;
readonly client = new ApiAiClient({
accessToken: this.token
});
conversation = new BehaviorSubject<Message[]>([]);
constructor() { }
converse(userMsg: Message) {
this.update(userMsg);
return this.client.textRequest(userMsg.message).then( (res) => {
const speech = res.result.fulfillment.speech;
const botMsg: Message = {
userId: 'bot',
message: speech,
name: 'DialogFlow',
timestamp: new Date()
};
this.update(botMsg);
});
}
update(msg: Message) {
this.conversation.next([msg]);
}
}
services/chat.service.ts
...[略]...
<ion-footer>
<form #myForm="ngForm">
<ion-item>
<ion-label position="floating">輸入訊息</ion-label>
<ion-input [(ngModel)]="message" name="message"
(keyup.enter)="onSubmit(); myForm.reset()" type="text"
required>
</ion-input>
</ion-item>
</form>
</ion-footer>
home.page.html
❶ 用ngForm 建立表單(html檔)
"ngForm": 建立表單的FormGroup, 表單名稱為myForm
(keyup.enter):angular鍵盤事件繫結, 設定事件處理器為onSubmit()
[(ngModel)]: 雙向繫結,建立表單欄位與屬性message的繫結關係
import { Message } from './../_model/message';
...[略]...
export class HomePage implements OnInit {
user = {
userId: 'G00001',
displayName: 'Guest'
};
message: string; // 雙向繫結屬性,紀錄輸入欄位資料
ngOnInit() {}
...[略]...
}
❷ 定義雙向繫結屬性、表單處理器
home.page.ts
import { ChatService } from './../services/chat.service';
...[略]...
export class HomePage implements OnInit {
...[略]...
constructor(private chat: ChatService) {}
// enter 鍵按下, 送出訊息
onSubmit() {
if (!this.message) { return; }
const msg: Message = {
userId: this.user.userId,
name: this.user.displayName,
message: this.message,
timestamp: new Date()
};
this.sendMessage(msg);
}
// 送出訊息到chat service
sendMessage(msg: Message) {
this.chat.converse(msg);
}
}
❷ 定義雙向繫結屬性、表單處理器
home.page.ts
❸ 訂閱(ts檔) 與串接資料
scan: 逐一檢視資料流每一個項目
訂閱對話資料流
資料進來, 逐一檢視, 串接(concat)每筆資料
...[略]...
import { Observable } from 'rxjs';
import { scan } from 'rxjs/operators';
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {
...[略]...
messages$: Observable<Message[]>;
ngOnInit() {
this.messages$ = this.chat.conversation.asObservable()
.pipe(
scan( (acc, val) => acc.concat(val) )
);
}
}
建立Observable($號是慣用法,代表這是個Observable)
home.page.ts
import { ChatService } from './../_services/chat/chat.service';
import { Message } from './../_model/message';
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { scan } from 'rxjs/operators';
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {
user = {
userId: 'G00001',
displayName: 'Guest'
};
message: string;
messages$: Observable<Message[]>;
constructor(private chat: ChatService) {}
ngOnInit() {
this.messages$ = this.chat.conversation.asObservable()
.pipe(
scan( (acc, val) => acc.concat(val) )
);
}
// enter 鍵按下, 送出訊息
onSubmit() {
if (!this.message) { return; }
const msg: Message = {
userId: this.user.userId,
name: this.user.displayName,
message: this.message,
timestamp: new Date()
};
this.sendMessage(msg);
}
// 送出訊息到chat service
sendMessage(msg: Message) {
this.chat.converse(msg);
}
}
home.page.ts (完整)
...
<ion-content padding>
<ion-list>
<ion-item *ngFor="let msg of messages$ | async">
<ion-button [color]="msg.userId === 'bot' ? 'danger' : 'primary' "
[ngClass]="msg.userId==='bot' ? 'button-left' : 'button-right'">
{{ msg.message }}
</ion-button>
</ion-item>
</ion-list>
</ion-content>
...
home.page.html
❹ 顯示
async: 非同步資料流
使用者輸入: 預設顏色, 置右
機器人回應: danger顏色, 置左
<ion-header>
<ion-toolbar>
<ion-title>聊天</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<ion-list>
<ion-item *ngFor="let msg of messages$ | async">
<ion-button [color]="msg.userId === 'bot' ? 'danger' : 'primary' " [ngClass]="msg.userId==='bot' ? 'button-left' : 'button-right'">
{{ msg.message }}
</ion-button>
</ion-item>
</ion-list>
</ion-content>
<ion-footer>
<form #myForm="ngForm">
<ion-item>
<ion-label position="floating">輸入訊息</ion-label>
<ion-input [(ngModel)]="message" name="message" (keyup.enter)="onSubmit(); myForm.reset()" type="text" required>
</ion-input>
</ion-item>
</form>
</ion-footer>
home.page.html(完整)
form {
ion-item {
--ion-item-background: #3880ff;
--ion-item-color: #ffffff;
}
}
.button-left {
margin-right: auto;
}
.button-right {
margin-left: auto;
}
home.page.scss(完整)
DialogFlow API
程式 或 內建回應字串
設定某些想要回應的項目
輸入AI預備回應的訊息