RevComm Tech Blog

コミュニケーションを再発明し 人が人を想う社会を創る

MiiTel Outgoing Webhook の使い方: タスク管理ツールとの連携サンプル

1. はじめに

こんにちは、RevComm でバックエンドエンジニアとしてプロダクトの開発に携わっている池畠です。

2022 年 12 月下旬に弊社から MiITel の新機能として Outgoing Webhook をリリースしました。今回は弊社における Outgoing Webhook を活用した業務の効率化事例について、Python によるマイクロサービスを実装しながら紹介していこうと思います。

2. Outgoing Webhook

MiiTel の Outgoing Webhook は、応対履歴の音声解析完了時に、設定した URL (Webhook 送信先サーバー) に HTTP リクエストを送信する機能です。開発者は、Webhook を利用して外部システムへ MiiTel 応対履歴情報を連携できます。

詳しい設定方法・連携内容は サポートページ をご参照ください。

3. 運用に向けた課題

弊社のカスタマーサポートではお客様からのお電話での対応に MiiTel を活用しております。MiiTel には既に Salesforce や kintone, HubSpot といった外部の CRM に応対履歴情報の連携機能は備わっていますが、問い合わせ内容のチケット管理には Asana を活用しているため、手動で Asana タスクを作成して応対履歴内容を転記するひと手間が要りました。

そこで、Outgoing Webhook によって MiiTel 応対履歴情報から自動で Asana タスクを作成するマイクロサービスを構築し、チケット作成を自動化するような業務改善の事例をご紹介します。

4. Asana 側の設定

Asana にタスクを自動作成するためには、事前に以下の項目を取得し控えておく必要があります。本章では、それらの確認方法を紹介します。

  • API token
  • Workspace id
  • Project id

API token

まずは Asana の デベロッパーコンソール にアクセスしてログインします。

画面下部の「トークンを新規作成」をクリックし、「API 利用規約に同意しますにチェック」を入れ、トークン名を入力します。

入力が完了したらトークンを作成ボタンを押下すると、トークンをコピーのモーダルが出現するので「コピー」ボタンを押下してすぐに参照できるように控えておきます。

「必ずここでこのアクセストークンをコピーしてください。二度と表示されません。」という表示もありますので紛失しないようにしましょう。「完了」を押下して API token の取得は完了です。

Workspace id

まずは Asana のトップページにアクセスします。
ご自身のアイコンをクリックすると「所属先組織について...」が選択できるのでこれを押下すると別ウィンドウに遷移します。

そのときの URL を確認すると
https://app.asana.com/admin/{数字}/overview のようになっており、 admin と overview に挟まれている数字が Workspace id に相当するのでこれを控えておきましょう。

Project id

本記事ではデモとして新規に Asana プロジェクトを作成してみます。
まずは Asana のトップページにアクセスします。

「プロジェクトを作成」をクリックし、「テンプレートを使用」を選んでいきます。

今回は、「Support チームへの機能要望のお問い合わせを Developer チームが確認しやすいようなチケット化をする」というユースケースを想定して、「IT リクエスト」のテンプレートを選択していきます。「テンプレートを使用」をクリック。

プロジェクトの詳細を追加していきます。プロジェクト名を記入し、チームを選択して「プロジェクトを作成」ボタンを押下します。

プロジェクトの作成が完了すると https://app.asana.com/0/{数字}/board という URL に遷移します。0 と board に挟まれている数字が Project id に相当するのでこれを控えておきましょう。

5.Chalice によるマイクロサービス構築

本章では、Python の AWS マイクロサービス構築用バックエンドフレームワークである Chalice を活用して Outgoing Webhook の受け口と Asana API を実行する Lambda を作成していきます。

筆者の環境

  • MacBook Pro (13-inch, M1, 2020)
  • Python 3.10.2
  • Mac OS Ventura

導入

Chalice, Asana の Python 用モジュールのインストールは、下記のコマンドでできます。

pip install chalice asana

続いて Chalice の新規プロジェクトを作成します。ディレクトリを移動し、Lambda へのデプロイ用に pip freeze をしてファイルに出力させておきます。

chalice new-project helloworld
cd helloworld
pip freeze > requirements.txt

認証周辺の実装

本機能における「認証」とは初回リクエストで "challenge" キーの値をサーバー側でレスポンスするということになります。その部分を実装していきます。

現在のディレクトリに app.py というファイルがありますが、これがアプリケーションの本体になります。まず、例として認証周りの処理を下記のように記述していきます。

import json
import os

import asana
from chalice import Chalice, ForbiddenError, Response

app = Chalice(app_name='helloworld')

@app.route("/", methods=["POST"])
def index():
    request = app.current_request
    body = request.json_body
    headers = request.headers

    # コメント 1
    if headers.get("X-MiiTel-Outgoing-Webhook-Key") != "OUTGOING_WEBHOOK_KEY":
        raise ForbiddenError("Invalid header key")

    if "challenge" in body.keys():
        return Response(
            body=body["challenge"],
            status_code=200,
            headers={"Content-Type": "text/plain"},
        )

まずは Outgoing Webhook で設定予定の追加ヘッダーに該当する箇所を見ていきます。コメント 1 の箇所におきまして、”OUTGOING_WEBHOOK_KEY” という値があるかを確認しています。ハードコードは良くないので実運用においては環境変数にするか、AWS Systems Manager (SSM) や AWS Secrets Manager に格納した値を参照するなどしておく必要があります。存在しなければ 403 エラーに該当する FobiddenError を raise してその原因を軽く引数の文字列に記しておきます。次にポイントとなるのが、body 内に ”challenge” というキーが存在するかを確認するということです。初回リクエストではこの ”challenge” キーの値をそのままレスポンスで返す必要があるので早めに return しておきます。

応対履歴の整形

次は応対履歴から Asana タスクを作成する処理を以下のコードでしていきます。Outgoing Webhook ペイロードの形式に関してはサポートページにも記載がありますので詳しくはこちらを参照ください。

    # コメント 2
    success_response = Response(
        body="Success", status_code=200, headers={"Content-Type": "text/plain"}
    )

    call_type = body["call"]["details"][0]["call_type"]

    # コメント 3
    if call_type not in ("INCOMING_CALL", "OUTGOING_TRANSFER", "QUEUEING_CALL"):
        return success_response

    comments = body["call"]["details"][0]["comments"]

    # コメント 4
    if not comments:
        return success_response
    sorted_comments = sorted(comments, key=lambda x: x["created_at"])
    first_comment = sorted_comments[0]["value"]
    second_comment = sorted_comments[1]["value"] if len(sorted_comments) > 1 else ""

    tags = [
        t["value"]
        for t in list(
            filter(
                lambda x: x["value"]
                in (
                    "[OW] 緊急度: 高",
                    "[OW] 緊急度: 中",
                    "[OW] 緊急度: 低",
                    "[OW] 要望度: 高",
                    "[OW] 要望度: 中",
                    "[OW] 要望度: 低",
                ),
                body["call"]["details"][0]["tags"],
            )
        )
    ]

    # コメント 5
    if not tags:
        return success_response
    sorted_tags = sorted(tags)
    first_tag = sorted_tags[0]
    second_tag = sorted_tags[1] if len(sorted_tags) > 1 else ""

    participants = {
        p["from_to"]: p["company_name"]
        for p in body["call"]["details"][0]["participants"]
    }

    company_name = participants["FROM"]
    speech_recognition_summary = body["call"]["details"][0]["speech_recognition"]["raw"]
    url = f'https://{body["call"]["tenant_code"]}.miitel.jp/app/calls/{body["call"]["details"][0]["id"]}'

    priority_and_urgency = {
        "[OW] 緊急度: 高": "高",
        "[OW] 緊急度: 中": "中",
        "[OW] 緊急度: 低": "下",
        "[OW] 要望度: 高": "高",
        "[OW] 要望度: 中": "中",
        "[OW] 要望度: 低": "下",
    }
    urgency = priority_and_urgency.get(first_tag, "")
    priority = priority_and_urgency.get(second_tag, "")

    note_template = """▼顧客が言っている要望詳細(もしあれば録画・録音):
    企業名: {company_name}
    応対履歴: {url}

    <原文ママ>
    {speech_recognition_summary}

    ▼なぜそう言っているか?どのような顧客か?解釈を記入ください:
    {second_comment}

    ▼要望度合い(高・中・下):{priority}
    ▼緊急性(高・中・下):{urgency}"""

    notes = note_template.format(
        company_name=company_name,
        url=url,
        speech_recognition_summary=speech_recognition_summary,
        second_comment=second_comment,
        priority=priority,
        urgency=urgency,
    )

コメント 2 の箇所では返却用のレスポンスオブジェクトを変数化しています。
Asana への連携する・しない/成功・失敗に関わらずこの値を返していきます。
お客様から受けた問い合わせを連携する想定なのでコメント 3 の箇所の分岐処理では着信系以外の応対種別を省いています。
コメント 4 の箇所の分岐では通話中のメモ用にとったコメントが存在しなければ連携なしとしています。
同じくコメント 5 の箇所の分岐では緊急度や優先度の応対メモがなかった場合にも連携なしとします。
その他取引先会社名・音声認識結果の要約・応対履歴 URL を変数に格納し、Asana の notes に挿入するテンプレートに埋め込みます。

Asana へのリクエスト

最後に下記のようになります。

    try:
        client = asana.Client.access_token(asana_token)
        client.tasks.create_task(
            {
                "workspace": "workspace_id",
                "projects": [
                    "project_id",
                ],
                "name": first_comment,
                "notes": notes,
            },
            opt_pretty=True,
        )
    except:
        pass

    return success_response

asana モジュールにて asana_token から access_token のオブジェクトを取得します。
これまでの処理で得られた workspace_id, project_id, first_comment, notes を引数に task を作成する関数を実行します。
例外処理はエラー確認用に出力しておくと良いですが今回は pass にて割愛します。最後に 200 レスポンスを返却することで実装は完了です。

デプロイ

Chalice のデプロイは下記コマンドからできます。

chalice deploy

処理が終了すると api gateway の URL が発行されるのでそれを控えておきましょう。

6. MiiTel 側の設定

前章で Asana タスク自動作成を行うマイクロサービスを構築し、curl による疎通が確認できたので、発行された URL を MiiTel 側に設定していきます。
サポートページの引用になりますが、以下のように設定画面にアクセスします。

  1. 管理者権限があるユーザーで MiiTel Admin にログインします。 (MiiTel Admin にアクセスするには、MiiTel Analytics 画面右上の歯車アイコンをクリックします)
  2. [外部連携] > [Outgoing Webhook] を選択します。
  3. [連携設定を追加] をクリックします。

各項目を設定していきますが今回のケースでは添付画像のような設定にしていきたいと思います。

設定時のポイントとしては以下になります。

  • 追加ヘッダーを設定する
  • リクエストの軽量化のため、音声認識結果のフレーズとキーワードをペイロードから除外する

[保存] をクリックして設定は完了です。

7.着信〜音声認識〜タスク作成まで

MiiTel Phone を起動した状態で着信通話をしてみます。まず、UI をワイドモードに変更し、「電話に出る」を押下すると通話情報が確認できます。

続いて「コメント」タブに移動し、Asana タスクのタイトルを記入して保存します。

次に「応対メモ」から緊急度・要望度タグを選択して保存しておきます。

通話が終了したときに応対メモとしてチェックした内容が正しいかを確認して保存、コメントも同様に保存を再度押下します。

作成された応対履歴に移動し、コメントからこの応対における補足内容を記入します。

音声解析が完了すると IT リクエストプロジェクトに Asana タスクが作成され、記入内容・音声認識結果が反映されていることが確認できました。

8.まとめ

本記事では、Outgoing Webhook の受け口として Chalice でマイクロサービスを構築し、Asana タスクを作成する実装と手順を紹介しました。 音声認識結果の要約が見れることによって、 MiiTel の応対履歴を閲覧せずとも要望背景の把握に活用することができそうですね。 お客様のユースケースに応じて自由にアレンジが可能な Outgoing Webhook をぜひご活用ください。