まくろぐ

チャットボット: Bot Builder SDK で会話の状態を保存する (Storage)

更新:
作成:

ボットは基本的にステートレスで動作するので、ユーザとの会話のコンテキストを把握するには、ステートの管理を明示的に行う必要があります。 Bot Builder SDK にはそのためのユーティリティクラスが用意されています。 ここでは、Node.js の botbuilder パッケージを使って説明します。

Storage インタフェース

botbuilder パッケージに含まれている Storage インタフェースは、抽象化されたストレージに JSON オブジェクトを保存するための API を定義しています。 writereaddelete の 3 つの API のみなのでとてもシンプルです。

write メソッド

JSON オブジェクトをストレージに保存するための API です。 オブジェクトを保存するときには、名前(キー)を付けて、キー&バリューの形のオブジェクトとして保存します。 下記の例では、保存したい state オブジェクトに、botState という名前を付けて保存しています。

state.topic = 'someTopic';
await storage.write({ 'botState': state });

read メソッド

ストレージに保存されたオブジェクトを読み出すための API です。 読み出したいオブジェクトの名前を配列で渡すと、オブジェクトの連想配列が返ってきます。 下記の例では、botState という名前で保存されたオブジェクトを、state 変数に取り出しています。

const items = await storage.read(['botState']);
const state = items['botState'] || {};

delete メソッド

ストレージに保存されたオブジェクトを削除するための API です。 削除したいオブジェクトの名前を配列で渡します。

await storage.delete(['botState']);

MemoryStorage クラス

実際にストレージを扱うには、Storage インタフェースを実装したクラスが必要です。 MemoryStorage クラス はローカルテスト用のインメモリストレージで、ボットのプロセスが終了するまで情報を保持します。

MemoryStorage のインスタンス化
const { MemoryStorage } = require('botbuilder');
const storage = new MemoryStorage();

テスト実装

下記はローカル環境で MemoryStorage の振る舞いをテストするためのシンプルなボット実装です。 ユーザがメッセージを送るたびに、state オブジェクトに保持したカウンタの値が 1 ずつ増えていきます。

ローカルテスト用のボット実装
const { ActivityTypes, BotFrameworkAdapter, MemoryStorage } = require('botbuilder');

// ストレージインスタンスの生成
const storage = new MemoryStorage();

// ストレージにアクセスカウンタを保存
async function onTurn(context) {
  const items = await storage.read(['state']);
  const state = items['state'] || { 'count': 0 };
  await context.sendActivity(`state.count = ${state.count}`);
  state.count += 1;
  await storage.write({'state': state});
}

// 以下ほぼテンプレート
const adapter = new BotFrameworkAdapter({});
adapter.onTurnError = async (context, error) => {
   console.error(`[onTurnError]: ${error}`);
};

const server = require('restify').createServer();
server.post('/api/messages', (req, res) => {
  adapter.processActivity(req, res, async (context) => {
    if (context.activity.type === ActivityTypes.Message) {
      await onTurn(context);
    }
  });
});
server.listen(process.env.port || process.env.PORT || 3978, () => {
  console.log(`Bot server listening to ${server.url}`);
});
/p/3fnyk44/storage-001.png

別の Storage 実装

上記の例ではインメモリのストレージ実装として MemoryStorage クラスを使用しましたが、会話の状態を永続化するには別のストレージ実装を使用する必要があります。 例えば、Azure Blob Storage や Cosmos DB を使用するためのストレージ実装が用意されています。

(おまけ)ActivityHandler を使った実装

下記は、上記のボットプログラムを ActivityHandler を使って実装し直したものです。 ボットのメイン処理をボットクラスとして実装すると、何をするボットなのかがわかりやすくなります。

ローカルテスト用のボット実装
const { ActivityHandler, BotFrameworkAdapter, MemoryStorage } = require('botbuilder');

// ボット実装
class MyBot extends ActivityHandler {
  constructor() {
    super();

    // ストレージインスタンスの生成
    this.storage = new MemoryStorage();

    // イベントハンドラの登録
    this.onMessage(async (context, next) => {
      // ストレージにアクセスカウンタを保存
      const items = await this.storage.read(['state']);
      const state = items['state'] || { 'count': 0 };
      await context.sendActivity(`state.count = ${state.count}`);
      state.count += 1;
      await this.storage.write({'state': state});

      await next();
    });
  }
}

// 以下ほぼテンプレート
const bot = new MyBot();
const adapter = new BotFrameworkAdapter({});
adapter.onTurnError = async (context, error) => {
   console.error(`[onTurnError]: ${error}`);
};

const server = require('restify').createServer();
server.post('/api/messages', (req, res) => {
  adapter.processActivity(req, res, async (context) => {
    await bot.run(context);
  });
});
server.listen(process.env.port || process.env.PORT || 3978, () => {
  console.log(`Bot server listening to ${server.url}`);
});

コンテキストごとに状態を保存する

ここまで見てきたように、Storage インタフェースを使用すると、複数の Activity(メッセージのやりとり)をまたぐように状態を保存できるようになります。

しかし、これはいわゆるグローバルな状態保持であり、会話ごとの状態や、ユーザーごとの状態を保存するためには追加の仕組みが必要になります。

Bot Builder SDK には、そのような用途を想定した BotState クラス が用意されています。 下記の記事では BotState クラスの使い方を説明します。

関連記事

まくろぐ
サイトマップまくへのメッセージ