Twilio Flexで誰も応答できない場合の処理

:::note alert
2023年5月1日を持ちまして、株式会社KDDIウェブコミュニケーションズのTwilioリセール事業が終了したため、本記事に記載されている内容は正確ではないことを予めご了承ください。
:::

# はじめに

みなさん、こんにちは。
KDDIウェブコミュニケーションズの Twilioエバンジェリストの高橋です。

こちらは、今年の８月頃に投稿した記事を、[Twilio Flex Advent Calendar 2021](https://qiita.com/advent-calendar/2021/twilio-flex) 12日目向けに少しリライトしたものとなります。

[前回](https://qiita.com/mobilebiz/items/971683edee076b3a5686)同様、Twilio FlexのACD機能で少し複雑なTipsのご紹介となります。

# シナリオの確認

今回のシナリオは以下のとおりです。

- 着信時に、対応できるオペレータが誰もいない場合に、特定のメッセージを流して通話を切断したい。

このようなニーズの場合、大きく２つのアプローチがあります。

- 着信時にオペレータを確認して、対応できるオペレータがいない場合はメッセージを流して切断。
- 一度待ち呼を生成して、一定時間でオペレータが対応できなかった場合にメッセージを流して切断。

前者のケースでは、TaskRouterに流す前に判定してしまうのがよいです。一方、後者のケースでは一度TaskRouterに流して、誰も対応できなかったことを検知して対応します。

# 具体的な対応方法

ではこれらの対応策について具体的に見ていきましょう。

## TaskRouterに流す前に判定する

こちらは、Studioの中でFunctionsを呼び出すことで対応します。

呼び出すFunctionsには、例えば以下のようなコードを書きます。

```javascript:特定のスキルを持ったワーカーの検索
/**
 * 特定のスキルを持ったワーカーが着信可能かを確認する
 * @params {String} skill 検索対象のスキル（例:support）
 */
exports.handler = function(context, event, callback) {
    const WORKSPACES_SID = context.WORKSPACES_SID; // 環境変数からワークスペースのSIDを読み込む
    const skill = event.skill.toLowerCase() || '';
    const client = context.getTwilioClient();
    client.taskrouter.workspaces(WORKSPACES_SID).workers.list({
        targetWorkersExpression: `routing.skills HAS '${skill}'`,
        available: true
    })
    .then(workers => {
        callback(null, { available: workers.length > 0 ? true : false });
    })
    .catch(err => {
        callback(err);
    });
};
```

この例ではFunctionsの呼び出し時に`skill`パラメータを渡していますが、この辺りは自由に変えてください。
いずれにしろ、このFunctionsの戻り値が`true`であれば対応できるオペレータがいることになります。

Studio内のフローは以下のような感じになります。

![スクリーンショット 2021-12-20 13.45.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/8e65e12e-b9db-5fb1-4a11-2a01e76ac7b4.png)

- 最初に営業宛なのかサポート宛なのかを確認し、その内容に沿って変数`skill`に値をセットします。
- その値をパラメーターとして、先程のFunctionを呼び出し、対応できるオペレーターの有無をチェックします。
- オペレーターが存在していればTaskRouterに引き渡し、存在しない場合は不在メッセージを流して切断します。

いずれにしろ、この方法はTaskRouterに入る前に処理をしてしまうものです。

## 待ち呼を作ってから処理を行う

こちらのシナリオでは、着信時にはオペレータの確認をするのではなく、一度TaskRouterにコールを送ります。
その後、TaskRouterの中の機能をつかって誰も出なかったことを検知します。

まずは、Twilio FlexにおけるTaskRouterのコールの処理フローについて簡単に説明します。

- Studio内で、SendFlexウィジェットにコールをつなぎます。このときに、TaskRouter内に定義してあるワークフローのどれを使うか指定します。
- 内部的には、この時点で「タスク」が生成されます。そして、このタスクが「ワークフロー」に渡されます。
- タスクを受け取ったワークフローは、このタスクを処理できる「キュー」を見つけます。キューは待ち呼を処理するためのしくみで、対応する「ワーカー」（オペレータのことです）のアサインを受け持ちます。
- アサインされたワーカーには着信の通知が届きます。この時点でタスクは「予約済み」となり、ワーカーがタスクを承諾するか、拒否するか、タイムアウトになるのを待ちます。
- ワーカーがタスクを承諾することで、ようやく着信呼がオペレータに繋がります。

さて、ここからが本題です。

今回のシナリオでは、誰も取らなかった場合にメッセージを流してコールを切断したいというものですから、そもそも誰も取らなかったということを検知しなくてはいけません。誰も取らなかったというのも正確には２つのシナリオがあります。

- オペレータのアサインはできて、呼び出しをしたけれどオペレータが反応しなかった場合
- そもそもオペレータがアサインできなかった場合

まず、前者の場合はタスク自体は「予約済み」になっているので、タイムアウトを待つことになります。タイムアウト値はデフォルトで120秒です。この値はワークフローのプロパティで変更ができます。

![スクリーンショット 2021-09-12 11.10.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/662c69e3-d583-c459-d302-ededa95f511b.png)

タイムアウトになると、ワークフローはそのオペレータのアクティビティをオフライン（デフォルト）にし、別のオペレータをアサインしにいきます。その後も誰もアサインできなかった場合は、後者と同じ動きとなります。

## ワークフローの設計

ここでもう一つ、ワークフローの設計についても確認しましょう。

![WorkFlow (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/b4843cf6-813b-5612-59de-2c774a44edef.png)

この図は、以下のシナリオに基づいて作られたワークフローの定義例です。

*「営業への問い合わせを処理するため、まずは営業担当者を呼び出す。営業担当者が不在、もしくは対応中の場合は、待ち呼にはせず他の担当者に回す。他の担当者も不在、もしくは対応中の場合は、10秒間だけ待ち呼にする。」*

①で、タスクのアトリビュートにある`skill`が`Sales`の場合という条件を指定しています。この条件に合致するタスクは、その下で指定しているように、まずは「sales」というタスクキューに入れられます。
ただし、このときに②の条件が参照されます。ここでは、`workers.available == 0`という条件が指定されているので、この条件（salesに対応できるワーカーが不在）に合致するとキューには入れられずに、次に進みます。
次は、「Everyone」というタスクキューです。ここでは、③に条件が指定されていないので、着信呼は④で指定した時間だけ、待ち呼としてキューに滞留します。この間にオペレータが開放されれば、自動的にそのオペレータにアサインされます。
④の時間内にオペレータがアサインできないと、ワークフローは一番下のタスクキュー（⑤）を利用します。
ここではタイムアウトが設定できないので、⑤にタスクキューが指定されていると誰かオペレータがアサインされるまで待ち呼の状態が続きます。
ただし、上記のように⑤が指定されていない（None）場合、ここではじめてワークフローは「タイムアウトイベント」を発行します。

よって、このようにワークフローを設計し、誰も取らなかった場合はタイムアウトを起こさせることで、誰も取らなかったことを検知します。

# タイムアウトイベントの取得とその後の処理

では、ワークフローのイベントはどのように取得すればよいでしょうか。

答えは、管理コンソールのワークスペース（ワークフローではないです）のSettingページに設定があります。

![スクリーンショット 2021-09-12 12.22.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/e5c38b88-48af-cb31-4cf5-db6d7cb84111.png)

このURLに対して、TaskRouterの各イベントが通知されるので、ここで、`workflow.timeout`イベントを取得すればよいです。

## 取得したあとはどうする

タイムアウトイベントには、現在キューに入っているコールのCallSidがタスクの属性（TaskAttributes）に入っているので、それを使ってコールのUpdateを行います。すなわち、別のTwiMLにリダイレクトするわけです。
例えばTwilio Functionsで実装すると以下のようになります。

```javascript:イベントコールバックでの処理
exports.handler = async function (context, event, callback) {
  console.log(`🐞 event callback.`);
  console.dir(event);

  if (event.EventType === "workflow.timeout") {
    // キューに入っているコールをリダイレクト
    const client = context.getTwilioClient();
    const call = await client
      .calls(JSON.parse(event.TaskAttributes).call_sid)
      .update({
        method: "POST",
        url: `https://${context.DOMAIN_NAME}/no-worker`, // ここでリダイレクト先のTwiMLを返す
      });
    console.log(`🐞 call update. ${call.sid}`);

    callback(null, {});
  } else {
    callback(null, {});
  }
};
```

リダイレクト先のFunctionsはこんな感じになります。

```javascript:メッセージを流す
exports.handler = async function (context, event, callback) {
  const client = context.getTwilioClient();
  const twiml = new Twilio.twiml.VoiceResponse();

  // 不在のメッセージを流す
  twiml.say(
    {
      language: "ja-JP",
      voice: "Polly.Mizuki",
      loop: "1",
    },
    "申し訳ございませんが、ただいま対応できるオペレータが不在です。お手数ですが、しばらくしてからおかけ直しください。"
  );
  callback(null, twiml);
};
```

今回はメッセージを流して切断されますが、たとえば`Dial`などをつかって別の電話に転送させたり、`Record`を使って録音させたりすることもできます。

# まとめ

Twilio Flexをカスタマイズする際には、TaskRouterは避けては通れない機能となります。今回のように、ちょっと違う使い方をする場合などは、TaskRouterのイベントコールバックは色々と役に立ちます。
`workflow.timeout`以外にも様々なイベントが通知されるので、TaskRouterのRestAPIや、VoiceのRestAPIなどをうまく利用することで、色々な処理を組み込むことができます。

★次の記事
[Twilio Flexの始め方（プラグイン準備編）](https://qiita.com/mobilebiz/items/eeca1ce20bb5b561b515)

---
### Twilio（トゥイリオ）とは

https://cloudapi.kddi-web.com
Twilio は音声通話、メッセージング（SMS /チャット）、ビデオなどの 様々なコミュニケーション手段をアプリケーションやビジネスへ容易に組み込むことのできるクラウド API サービスです。初期費用不要な従量課金制で、各種開発言語に対応しているため、多くのハッカソンイベントやスタートアップなどにも、ご利用いただいております。

自己紹介|　
:--|:--
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/b51a8b88-abd5-ce5e-195a-de3564ba2ae6.png" height='200' width='150'/>|高橋克己（Katsumi Takahashi） 自称「<font color='red'>赤い芸人</font>」<br />グローバル・インターネット・ジャパン株式会社　代表取締役<br />株式会社KDDIウェブコミュニケーションズ　Twilio事業部エバンジェリスト<br /><br />2001年より大手通信事業者の法人サービスの教育に携わり、企業における電話のしくみや重要性を研究。2016年よりTwilio事業部にジョインし、Twilioを使ったスマートコミュニケーションの普及活動を精力的に行っている。<br />2015 Hall of Doers<br />2019 Twilio Champions