LoginSignup
20
12

More than 5 years have passed since last update.

BotFramework "WebChat" の歩き方

Last updated at Posted at 2017-12-19

はじめに

Microsoftの提供する BotFrameworkでは、Bot Builder SDKを使ってBotサーバを書くと、Bot Connector Serviceが仲立ちして各Channel (Facebook Messenger, Slack, Skype etc) を通してbotと会話できるようになるのですが、そのChannelの1つに 「WebChat」(webサイトに導入できるwidget)があります。

本記事では、WebChatの導入方法とカスタマイズ方法について書きます。
(Botサーバ側の開発については他の記事を参照下さい)
architecture-resize.png
(https://docs.microsoft.com/en-us/bot-framework/media/how-it-works/architecture-resize.png)

WebChatを導入する方法

  • WebChat自体はReact-Redux製のOSSとしてこちら で公開されているのですが、以下に書くように自前でソースをbuild & ホスティングせず、Microsoftがホスティングするものを使って導入することもできます。

  • 導入法1: iframeをサイトに埋め込む

    • Botの管理ページ に「WebChat」Channelを追加し、表示される「Embed code」をコピーしてサイトに埋め込む
    • Embed codeの例 (シークレットはBotの管理ページから確認できます)
<iframe src='https://webchat.botframework.com/embed/{Bot名}?s={シークレット}></iframe>
  • 導入法2: iframeは使いたくない場合
    • 以下のように、WebChatの描画領域のDOMを渡す方法があります。
<!DOCTYPE html>
<html>
  <head>
    <link href="https://cdn.botframework.com/botframework-webchat/latest/botchat.css" rel="stylesheet" />
  </head>
  <body>
    <div id="bot"/>
    <script src="https://cdn.botframework.com/botframework-webchat/latest/botchat.js"></script>
    <script>
      BotChat.App({
        directLine: { secret: 'シークレット' },
        user: { id: 'userid' },
        bot: { id: 'botid' },
        resize: 'detect'
      }, document.getElementById("bot")); // ★chatを描画させたい領域のDOMを渡す
    </script>
  </body>
</html>

  • 導入法3: Reactサイトに導入する
    • npm install botframework-webchat した上で、以下のようにComponentとして追加することが出来ます
import { Chat } from 'botframework-webchat';

...

const YourApp = () => {
    <div>
        <YourComponent />
        <Chat directLine={{ secret: 'シークレット' }} user={{ id: 'user_id', name: 'user_name' }}/>
        <YourOtherComponent />
    </div>
}
...

その他詳細は README をご覧ください。

WebChatのカスタマイズ方法

開発フロー

# まずは環境構築
git clone https://github.com/Microsoft/BotFramework-WebChat.git
npm install

# => ソースを修正する

npm run build # => ./botchat.js, ./botchat.cssが吐き出される
npm run start # 開発サーバ起動

# => ブラウザで http://localhost:8000/samples/sidebar/index.html?s={シークレット} 
#    にアクセスしすると、 ./botchat.js, ./botchat.css を読み込んでchatが表示される

localのwidgetをlocalのBotサーバに接続させたい場合は?

以下のように、Bot Connector Serviceが参照するBotをngrokでlocalにポートフォワードすることで可能になります

# 上記手順でwidgetのサーバは起動した状態で

# 1. Botサーバを起動(Node.jsの場合)
npm run start # => port:3978でBotサーバが起動する

# 2. 別ターミナルからngrokを起動
ngrok http 3978 # => 「Forwarding  https://aaabbbccc999.ngrok.io -> localhost:3978」をメモっておく

# 3. https://dev.botframework.com/bots から対象のBotを選択 -> 
#    右上の「SETTINGS」 -> 「Messaging endpoint」を 2のURLに変更して保存
#    【※注意】2018年3月頃にMS Bot Frameworkは Azure Bot Serviceに統合されることが
#     アナウンスされているため、ここの手順は将来変わると思われます。

# 4. http://localhost:8000/... にアクセスしてBotと会話すると、localのBotサーバにつながります

見た目を変えたい場合

  • ヘッダーの色を変えたい、文字サイズを変えたい 程度であれば src/scss/botchat.scss を修正 & buildしたものをホスティングして読み込ませればokです

機能を修正したい場合

  • WebChat本体のソースコードを修正することになりますが、Bot Connector Serviceを介してBotサーバとやりとりをするという関係上、通信部分には手を出せない ( Direct Line API の制約を受ける) ことに注意が必要です。
    • Bot Connector Serviceを介さず、直接BotサーバのAPIを叩くんや!となれば話は別ですが、WebChat以外のChannelとの機能差異が生まれることになります。
    • DirectLineAPIとのやりとりは、BotFramework-DirectLineJSというライブラリとして提供されています。
  • ソースを読むにあたっては、TypeScript,React,Redux,redux-observable, RxJSあたりの知識が必要になります。(多い・・)

以下では、WebChat本体を修正するのに必要な知識についてざっくりまとめます。

ファイル構成

.
├── package-lock.json
├── package.json
├── samples # 動作確認用のHTML等が入っている
│   ├── backchannel
│   ├── fullwindow
│   ├── react
│   ├── sidebar
│   └── speech
├── src # コンポーネントの各ファイルは、ここに1階層(フラット)に配置されている
│   ├── ActivityView.tsx
│   :         :
│   ├── SpeechOptions.ts
│   ├── Store.ts   # store
│   ├── Strings.ts # localize文言はここ
│   ├── Timestamp.tsx
│   ├── Types.ts
│   ├── adaptivecards-hostconfig.d.ts
│   ├── getTabIndex.ts
│   └── scss # 見た目を変えたい場合はこの中のscssをいじる
├── test
├── tsconfig.json
├── webpack.config.js
└── webpack.production.config.js

ストア構成

action creator, state, reducerは src/Store.tsに定義

state名 役割
shell ユーザの入力したテキストを保持
format locale情報、localize用のテキストマスタを保持
size widgetのwidth, heightを保持
connection Bot Connector Serviceへのconnectionや、ユーザ/Botのidentity情報を保持
history Bot-ユーザ間でやりとりしたActivity(=メッセージや、タイピング中であることを通知するイベントetc)を保持する

createStoreは以下のようになっており、副作用を伴うものはEpicとして定義してMiddlewareにして混ぜ込む。
(Epic: actionのストリーム(=Observable)を受け取ってaction(Observable)を返す関数。例えば、ユーザがメッセージを入力して送信ボタンを押すと、Shell Actionの Send_Messageが発火 → それをsendMessageEpicでhookして state.history.botConnection.postActivity でBotConnector Serviceに投げる → 成功したら Send_Message_Succeed actionを返す という感じで使われる)

export const createStore = () =>
    reduxCreateStore(
        combineReducers<ChatState>({
            shell,
            format,
            size,
            connection,
            history
        }),
        applyMiddleware(createEpicMiddleware(combineEpics(
            updateSelectedActivityEpic,
            sendMessageEpic,    # <- 上記に書いたメッセージの送信は
            trySendMessageEpic, # <- これらで行う
            retrySendMessageEpic,
            showTypingEpic,
            sendTypingEpic,
            speakSSMLEpic,
            speakOnMessageReceivedEpic,
            startListeningEpic,
            stopListeningEpic,
            stopSpeakingEpic,
            listeningSilenceTimeoutEpic
        )))
    );

コンポーネント構成

  • src/App.tsxがReactアプリケーションを表現、src/Chat.tsxがTopレベルのコンポーネント
  • 見た目上はざっくり以下のような対応になる quiz2.png

○○したい時はこうしよう

ページ遷移しても同じ会話を継続したい

BotChat.Appのコンストラクタに渡す directLineオプションに conversationId というものがあり、これにページ遷移前のconversationIdを渡してchatを起動すると、会話を継続することができます。

BotChat.App({
    directLine: {
        secret: params['s'] || envToSecret[env],
        token: params['t'],
        domain: params['domain'],
        webSocket: isWebSocket,
        conversationId: 'hogehoge', // ★ここに渡す
    },
    user: user,
    bot: bot,
        :

(遷移前のページでの)conversationIdはcookie等に保存すると良いかと思います。
保存するタイミングとしては、BotからのActivityを受け取る箇所で行えば良いでしょう。

# https://github.com/Microsoft/BotFramework-WebChat/blob/master/src/Chat.tsx

diff --git a/src/Chat.tsx b/src/Chat.tsx
index 8e6a8c9..af17109 100644
--- a/src/Chat.tsx
+++ b/src/Chat.tsx
@@ -78,7 +78,14 @@ export class Chat extends React.Component<ChatProps, {}> {
         let state = this.store.getState();
         switch (activity.type) {
             case "message":
-                this.store.dispatch<ChatActions>({ type: activity.from.id === state.connection.user.id ? 'Receive_Sent_Message' : 'Receive_Message', activity });
+                let is_sent_message = activity.from.id === state.connection.user.id ? true : false;
+                this.store.dispatch<ChatActions>({ type: is_sent_message ? 'Receive_Sent_Message' : 'Receive_Message', activity });
+                if (is_sent_message) {
+                    break;
+                }
+                // 例えばこんな感じでconversationIdを保存
+                document.cookie = 'conversationId=' + activity.conversation.id;
+
                 break;

             case "typing":

Bot-Widget間で独自のメッセージ交換やイベント通知をしたい

  • 両者で交わされるActivityオブジェクト(メッセージやTyping中であることのイベント通知)に entityというプロパティ(anyの配列)があり、そこは自由に使えるようです。
    • 使うシーンとしては、entity経由でwidgetからBotにlocation.hrefを通知して、chatが起動されたページによってBotの応答を変える、など色々考えられそうです。
    • entityはBot Connector Serviceを経由する、かつキャッシュされる(上に書いたconversationIdを渡してchatを起動した際は、キャッシュされたactivityがConnector Serviceから送られてくる)ため、外部サービスに渡して良い情報なのか?という点は考慮が必要かと思います。
  • Bot側(Node.js SDK)のReferenceは これこれです。 以下のような感じでentityを参照/送信できます。
var builder = require('botbuilder');

function(session, args){
    // ユーザから送られたentitiesを参照
    var entities = session.messsage.entities;
                 :
    // ユーザにentitiesを送る
    var entity = {hoge: 'fuga'};
    var message = new builder.Message(session)
        .text('hoge')
        .addEntity(entity)
    session.send(message)
  • widget側では以下のようにして参照します。
// 例. ActivityView
this.props.activity.entities

  • widget側からentityを送る方法は未実装のようですが、以下の様な改修すると送ることができます。(本家へのPR検討しよう・・)
diff --git a/src/Store.ts b/src/Store.ts
index 13da314..56963cf 100644
--- a/src/Store.ts
+++ b/src/Store.ts
@@ -9,7 +9,7 @@ import * as konsole from './Konsole';

 import { Reducer } from 'redux';

-export const sendMessage = (text: string, from: User, locale: string) => ({
+export const sendMessage = (text: string, from: User, locale: string, entities: any[] = []) => ({
     type: 'Send_Message',
     activity: {
         type: "message",
@@ -17,7 +17,8 @@ export const sendMessage = (text: string, from: User, locale: string) => ({
         from,
         locale,
         textFormat: 'plain',
-        timestamp: (new Date()).toISOString()
+        timestamp: (new Date()).toISOString(),
+        entities: entities
     }} as ChatActions);

 export const sendFiles = (files: FileList, from: User, locale: string) => ({
  • sendMessageを使う側
import 'sendMessage' from 'path/to/Store';

const entities = [{hoge: 'fuga'}];
sendMessage('message', user, locale, entities);

以上です。素敵なchatbot生活をお過ごし下さい :innocent:

20
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
12