Ionic Tutorial

Lesson 10: Chatbot with DialogFlow

last updated: 2022/06/08

ChatBotApp

以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,必須架設公開網站)

Part 1. 建立Agent代理人

前往 Dialogflow平台

https://dialogflow.cloud.google.com/

步驟1: 新建代理人 Create Agent (optional) (1/2)

Create Agent

步驟1: 新建代理人 Create Agent (optional)  (2/2)

輸入代理人名稱

設定預設語言別

綁定Google專案

步驟2: 建立意圖 (Create Intent)

測試主控台

建立新的意圖

預設Intents

已產生兩個Intents: Fallback, Welcome

輸入測試語句

何時觸發Welcome? 偵測到「開始對話」的語句

何時觸發Fallback? 偵測到agent不了解的語句

Intent指的是某句子中的意圖(intention)

我要預約

明天天氣預報

你們店怎麼去?

使用者字句

可能的意圖

預約

查詢天氣

查詢地址

步驟2-1: 制定Welcome意圖 (Create Intent)

2-1-1 客製化Welcome意圖:需符合系統目標、Bot的人設

❸存檔

❶ 刪除預設回應訊息

❷ 新增回應訊息如上

歡迎光臨!你想知道本店營業時間,還是想要預約來店?

步驟2-1: 制定Welcome意圖 (Create Intent)

❶ 多加幾種不同的歡迎詞

2-1-2 建立回應訊息的變形

❷ 存檔後測試

你好!我可以告訴你本店營業時間,也可以幫你預約,你需要什麼服務呢?

嗨!有什麼需要幫忙的嗎?我可以告訴你本店營業時間,也能幫你預約來店。

步驟2-2: 建立自訂意圖 (Custom Intent)

❶ 建立新的意圖

2-2-1 兩個工作: 告知營業時間、預約 ➔ 自訂意圖

步驟2-2: 建立自訂意圖 (Custom Intent)

❶ 輸入意圖名稱

2-2-1 兩個工作: 告知營業時間、預約 ➔ 自訂意圖

❷ 存檔

❸ 新增訓練用片語

步驟2-2: 建立自訂意圖 (Custom Intent)

❶ 輸入多個回應訊息

2-2-3 建立回應訊息

❷ 存檔

步驟2-3: 制定Fallback意圖 (Fallback Intent)

2-3 不了解使用者字句時,觸發Fallback意圖

❸存檔

❶ 刪除預設回應訊息

❷ 新增回應訊息如上

抱歉,請問你是要詢問營業時間?還是要預約來店呢?

不好意思,請問你是想知道營業時間?還是要預約呢?

步驟2-4: 建立「預約」自訂意圖 (Intent with parameters)

「預約」意圖必須能夠處理更複雜的句子

 辨識出3 PM, today為重要資訊

❷ 回應訊息附上剛提及的日期、時間資訊

Dialogflow要能辨識重要資訊、並將之儲存

參數(parameters), 每個辨識出來的參數都有自己的實體型別(entity type)

步驟2-4: 建立「預約」自訂意圖 (Intent with parameters)

2-4-1 新增「預約」意圖

❶ 輸入我要預約今天下午三點

❷ 系統自動偵測出兩個參數

手動標示兩個參數並選擇正確的entity type

❸存檔

步驟2-4: 建立「預約」自訂意圖 (Intent with parameters)

2-4-1 新增「預約」意圖(續)

❶ 回應訊息輸入上列文字

存檔

收到!為你預約 $date 時間是 $time 來店賞車,期待你的光臨。

步驟2-4: 建立「預約」自訂意圖 (Intent with parameters)

2-4-2 加入更多的訓練用片語

❶ 加入更多訓練用片語

存檔

我想看看最新款的單車

包含一些不含日期時間的句子

我的車壞了

步驟2-4: 建立「預約」自訂意圖 (Intent with parameters)

2-4-3 設定「參數」為必要資訊 (slot filling)

❶ 勾選為「必要」REQUIRED參數

我想看看最新款的單車

❷ 打開Define prompts,輸入後續提示字句

我的車壞了

使用者輸入的句子經常不包含日期、時間,要如何預約

❸存檔

步驟2-4: 建立「預約」自訂意圖 (Intent with parameters)

2-4-4 填入「參數」缺少時的提示訊息  (slot filling)

❶ 加入後續提示字句

以缺少日期為例,輸入後續Bot應該回應的問句

存檔

步驟2-4: 建立「預約」自訂意圖 (Intent with parameters)

2-4-4 填入「參數」缺少時的提示訊息  (slot filling)

❶ 加入後續提示字句

缺少「時間」時,後續Bot應該回應的問句

存檔

步驟2-5: 呼叫「預約」程式 (fulfillment using webhook)

預約若要記錄下來,需要外部程式(透過webhook)協助完成

Fulfillment運作示意圖

Fulfillment: 用來解決使用者要求的「外部服務」

(用量超過一定數量需付費)

步驟2-5: 呼叫「預約」程式 (fulfillment using webhook)

目標:Dialogflow收到「日期」、「時間」,記錄到Google日曆

❶ 開啟wehook call選項

存檔

前往「預約」intent

webhook外部服務:使用內建編輯器inline editor, 支援node.js程式

步驟2-5: 呼叫「預約」程式 (fulfillment using webhook)

❶ 開啟inline editor選項

fulfillments

❷ 在此處寫程式

❸ 部署

Part 2. 建立Webhook

Firebase cloud function

Heroku

...

部署支援https協定的公開網站

Part 3. 建立Ionic App

ChatBotApp專案

1. 紅色為user輸入 藍色為機器人回覆

smalltalk.greetings.hello

smalltalk.user.wants_to_talk

smalltalk.agent.good

2. 在DialogFlow設定了上述三個意圖

(intents)

ChatBotApp from scratch(1/4)

# 建立空白專案
ionic start ChatbotApp blank
cd ChatbotApp

# 安裝DialogFlow SDK
npm install @google-cloud/dialogflow

# 建立chat service
ionic g service services/chat

自然語言理解套件:google cloud dialogflow

https://github.com/googleapis/nodejs-dialogflow

Ⓐ 新增model資料夾
新增「資料模型.ts檔」

Ⓒ 建立服務

ionic g service 服務名稱

實作資料存取

ChatBotApp from scratch(2/4)

前往DialogFlow網站,以google帳號登入

建立agent(以Prebuilt Agents的Small Talk為原型)

選擇IMPORT

ChatBotApp from scratch(3/4)

V2 API,並找到Client access token

⦿

ChatBotApp from scratch(4/4)

修改src/environments/environment.ts檔

貼上client access token備用

export const environment = {
  production: false,
  firebaseConfig: {
    apiKey: ...
    ...
  },
  dialogflow: {
    angularBot: 'client access token貼於此'
  }
};

environments/environment.ts

ChatBotApp 建立chat service(1/6)

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

Ⓐ 新增資料模型

Ⓒ 實作服務

ChatBotApp 建立chat service(2/6)

...[略]...
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

ChatBotApp 建立chat service(3/6)

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) 訂閱、顯示

資料流

ChatBotApp 建立chat service(4/6)

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

ChatBotApp 建立chat service(5/6)

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;
  ...[略]...    
}

ChatBotApp 建立chat service(6/6)

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

ChatBotApp HomePage(1/8)

...[略]...
<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的繫結關係

ChatBotApp HomePage(2/8)

import { Message } from './../_model/message';
...[略]...
export class HomePage implements OnInit {
  user = {
    userId: 'G00001',
    displayName: 'Guest'
  };
  message: string;       // 雙向繫結屬性,紀錄輸入欄位資料
  
  ngOnInit() {}
  ...[略]...
}

❷ 定義雙向繫結屬性、表單處理器

home.page.ts

ChatBotApp HomePage(3/8)

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

ChatBotApp HomePage(4/8)

❸ 訂閱(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

ChatBotApp ChatPage(5/8)

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 (完整)

ChatBotApp ChatPage(6/8)

...
<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顏色, 置左

ChatBotApp ChatPage(7/8)

<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(完整)

ChatBotApp ChatPage(8/8)

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

DialogFlow API

程式 或 內建回應字串

設定某些想要回應的項目

輸入AI預備回應的訊息

Ionic Tutorial

By Leuo-Hong Wang

Ionic Tutorial

Chatbot with DialogFlow (part 2)

  • 1,133