オンラインで購入して店舗で受け取る: Bonjour Meal - パート 2 - ショッピング カートの作成

1. はじめに

53003251caaf2be5.png 8826bd8cb0c0f1c7.png

最終更新日: 2020 年 10 月 30 日

ビジネス メッセージにショッピング カートを作成する

シリーズ 2 番目のこの Codelab では、「オンラインで購入して店舗で受け取る」ユーザー ジャーニーの構築について学びます。多くの e コマース ジャーニーにおいて、ユーザーを有料ユーザーとして取り込むための成功の鍵を握っているのがショッピング カートです。また、ショッピング カートは、顧客についての理解を深め、顧客が興味のありそうな他の商品を提案することのできる手段でもあります。この Codelab では、ショッピング カートの構築と Google App Engine へのアプリケーションのデプロイを中心に学習していきます。

使いやすいショッピング カートとは

ショッピング カートは、オンライン ショッピング体験を成功に導くうえで重要な役割を果たします。ビジネス メッセージは、製品に関する Q&A を見込み顧客との間で円滑に進めながら、会話を通じて決済までの一連のショッピング体験をスムーズに進めることもできます。

9d17537b980d0e62.png

使いやすいショッピング・カートは、ユーザーがカテゴリ別に商品を閲覧したり、購入者が興味を持ちそうな他の商品を勧めたりすることができる快適なショッピング体験を実現します。ユーザーは、ショッピング カートに商品を追加した後でカート全体の内容を確認し、商品を削除 / 追加してから購入手続きに進むことができます。

作成するアプリの概要

Codelab シリーズのこのセクションでは、架空の会社 Bonjour Meal 用にパート 1 で作成したデジタル エージェントを拡張して、ユーザーが商品カタログを閲覧しながらショッピング カートに追加できるようにします。

この Codelab では、アプリで次のことをできるようにします。

  • ビジネス メッセージに質問一覧を表示する
  • 興味のありそうな商品をユーザーに提案する
  • ショッピング カートの内容を確認して合計金額の概要を作成する

ab2fb6a4ed33a129.png

学習内容

  • Google Cloud Platform 上の App Engine にウェブ アプリケーションをデプロイする方法
  • 永続ストレージ メカニズムを使用してショッピング カートの状態を保存する方法

この Codelab では、この Codelab シリーズのパート 1 で作成したデジタル エージェントを拡張する方法を説明します。

必要なもの

2. 設定方法

この Codelab は、最初のエージェントを作成し、Codelab のパート 1 を完了していることを前提としています。そのため、ビジネス メッセージ API および Business Communications API の有効化、サービス アカウント キーの作成、アプリケーションのデプロイ、Business Communications Console での Webhook の設定といった基本的なことについては説明しません。つまり、サンプル アプリケーションのクローンを作成して、このアプリケーションの基盤と整合性が保たれるようにし、Google Cloud Platform 上で Datastore の API がショッピング カートに関するデータを保持できるようにします。

GitHub からアプリケーションのクローンを作成する

ターミナルで、次のコマンドを使用して、Django Echo Bot サンプルのクローンをプロジェクトの作業ディレクトリに作成します。

$ git clone https://github.com/google-business-communications/bm-bonjour-meal-django-starter-code

サービス アカウント用に作成された JSON 認証情報ファイルをサンプルのリソース フォルダにコピーし、認証情報の名前を「bm-agent-service-account-credentials.json」に変更します。

bm-bonjour-meal-django-starter-code/bonjourmeal-codelab/step-2/resources/bm-agent-service-account-credentials.json

Google Datastore API を有効にする

この Codelab のパート 1 では、ビジネス メッセージ API、Business Communication API、Cloud Build API を有効にしました。

この Codelab では Google Datastore を使用するため、この API も有効にする必要があります。

  1. Google Cloud Console で Google Datastore API を開きます。
  2. 正しい GCP プロジェクトを使用していることを確認してください。
  3. [有効にする] をクリックします。

サンプル アプリケーションをデプロイする

ターミナルで、サンプルの step-2 ディレクトリに移動します。

ターミナルで次のコマンドを実行して、サンプルをデプロイします。

$ gcloud config set project PROJECT_ID*
$ gcloud app deploy
  • PROJECT_ID は、API への登録に使用したプロジェクトのプロジェクト ID です。

最後のコマンドの出力に記載されている、デプロイしたアプリケーションの URL をメモします。

Deployed service [default] to [https://PROJECT_ID.appspot.com]

デプロイしたコードには、ビジネス メッセージからメッセージを受信するための Webhook を持つウェブ アプリケーションが含まれています。これには、Codelab のパート 1 で設定したすべての内容が含まれています。まだ設定していない場合は、Webhook を設定してください。

このアプリケーションは、Bonjour Meal の営業時間など、簡単な問い合わせに返答します。モバイル デバイスでは、Business Communications Console の [エージェント情報] に記載されているテスト URL を使用して、このテストを実施してください。テスト URL をタップすると、モバイル デバイス上でビジネス メッセージ エクスペリエンスが起動し、エージェントと対話できます。

3. 商品カタログ

在庫管理システム

ほとんどの場合は、内部 API を介してブランドの在庫と直接統合できますが、ウェブページをスクレイピングしたり、独自の在庫追跡システムを構築したりする場合もあります。ここでは、在庫管理システムの構築について学習するわけではないので、エージェントの画像と商品情報を含む簡単な静的ファイルを使用します。このセクションでは、この静的ファイルから情報を引き出し、それを会話に表示して、ショッピング カートに追加できる商品をユーザーが閲覧できるようにします。

静的な在庫ファイルは次のようになります。

bonjourmeal-codelab/step-2/resources/inventory.json

{

    "food": [
        {
            "id":0,
            "name": "Ham and cheese sandwich",
            "price": "6.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/ham-and-cheese.png",
            "remaining": 8
        },
        {
            "id":1,
            "name": "Chicken veggie wrap",
            "price": "9.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/chicken-veggie-wrap.png",
            "remaining": 2
        },
        {
            "id":2,
            "name": "Assorted cheese plate",
            "price": "7.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/assorted-cheese-plate.png",
            "remaining": 6
        },
        {
            "id":3,
            "name": "Apple walnut salad",
            "price": "12.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/apple-walnut-salad.png",
            "remaining": 1
        }
    ]
}

それでは、Python アプリケーションでこのファイルを読み込みましょう。

在庫ファイルから読み取る

在庫ファイルは、./resources ディレクトリにある「inventory.json」という静的ファイルです。JSON ファイルの内容を読み取り、それを会話に表示するには、views.py に Python ロジックを追加する必要があります。JSON ファイルからデータを読み込み、商品の在庫状況のリストを返す関数を作成しましょう。

この関数の定義は、views.py の任意の場所に配置できます。

bonjourmeal-codelab/step-2/bopis/views.py

...
def get_inventory_data():
        f = open(INVENTORY_FILE)
        inventory = json.load(f)
        return inventory
...

これは、在庫ファイルからデータを読み取るために何が必要かを示しています。次は、この商品情報を会話に表示する手段が必要です。

商品カタログを表示する

わかりやすくするために、この Codelab では、一般的な商品カタログを用意し、1 つのリッチカード カルーセルを使って、すべての在庫商品をビジネス メッセージの会話に表示しています。

商品カタログを表示するためには、「Show Menu(メニューを表示)」というテキストと postbackData 「show-product-catalog」を使って返信の候補を作成します。ユーザーがこの返信の候補をタップし、ウェブ アプリケーションがポストバック データを受信すると、リッチカード カルーセルが送信されます。views.py の上部に、この返信の候補の新しい定数を追加します。

bonjourmeal-codelab/step-2/bopis/views.py

...
CMD_SHOW_PRODUCT_CATALOG = 'show-product-catalog'
...

ここでメッセージが解析されて、商品カタログを表示するリッチカード カルーセルを送信する新しい関数に転送されます。まず route_message 関数を拡張して、返信の候補がタップされたら関数「send_product_catalog」を呼び出すようにします。次に、この関数を定義します。

次のスニペットでは、route_message 関数の if ステートメントに条件を追加し、normalized_message が以前に定義した定数 CMD_SHOW_PRODUCT_CATALOG と等しいかどうかを確認します。

bonjourmeal-codelab/step-2/bopis/views.py

...
def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATALOG:
        send_product_catalog(conversation_id)
    else:
        echo_message(message, conversation_id)
...

フローを完成して send_product_catalog を定義しましょう。send_product_catalogget_menu_carousel, を呼び出します。これにより、前に読み込んだ在庫ファイルからリッチカードのカルーセルが生成されます。

関数の定義は、views.py の任意の場所に配置できます。以下のスニペットでは、ファイルの先頭に追加される新しい 2 つの定数が使用されています。

bonjourmeal-codelab/step-2/bopis/views.py

...

CMD_ADD_ITEM = 'add-item'
CMD_SHOW_CART = 'show-cart'

...

def get_menu_carousel():
    """Creates a sample carousel rich card.

    Returns:
       A :obj: A BusinessMessagesCarouselCard object with three cards.
    """

    inventory = get_inventory_data()

    card_content = []

    for item in inventory['food']:
        card_content.append(BusinessMessagesCardContent(
            title=item['name'],
            description=item['price'],
            suggestions=[
                BusinessMessagesSuggestion(
                    reply=BusinessMessagesSuggestedReply(
                        text='Add item',
                        postbackData='{'+f'"action":"{CMD_ADD_ITEM}","item_name":"{item["id"]}"'+'}'))

                ],
            media=BusinessMessagesMedia(
                height=BusinessMessagesMedia.HeightValueValuesEnum.MEDIUM,
                contentInfo=BusinessMessagesContentInfo(
                    fileUrl=item['image_url'],
                    forceRefresh=False))))

    return BusinessMessagesCarouselCard(
        cardContents=card_content,
        cardWidth=BusinessMessagesCarouselCard.CardWidthValueValuesEnum.MEDIUM)

def send_product_catalog(conversation_id):
    """Sends the product catalog to the conversation_id.

    Args:
        conversation_id (str): The unique id for this user and agent.
    """
    rich_card = BusinessMessagesRichCard(carouselCard=get_menu_carousel())

    fallback_text = ''

    # Construct a fallback text for devices that do not support carousels
    for card_content in rich_card.carouselCard.cardContents:
        fallback_text += (card_content.title + '\n\n' + card_content.description
                          + '\n\n' + card_content.media.contentInfo.fileUrl
                          + '\n---------------------------------------------\n\n')

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        richCard=rich_card,
        fallback=fallback_text,
        suggestions=[
        BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='See my cart',
                postbackData=CMD_SHOW_CART)
            ),
        BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='See the menu',
                postbackData=CMD_SHOW_PRODUCT_CATALOG)
            ),
        ]
        )

    send_message(message_obj, conversation_id)
...

カルーセル アイテムの作成を調べると、BusinessMessagesSuggestion クラスのインスタンスも作成されます。各提案は、カルーセル内の商品ごとにユーザーが選択する操作を表します。ユーザーが返信の候補をタップすると、ビジネス メッセージは、商品およびユーザー操作(カートへの商品の追加や削除)を記述した JSON を含む postbackData を Webhook に送信します。以下のセクションでは、次のようなメッセージを解析して、実際に商品をカートに追加できるようにします。

変更を加えたら、このウェブ アプリケーションを Google App Engine にデプロイして試してみましょう。

$ gcloud app deploy

モバイル デバイスに会話サーフェイスが読み込まれたら、「show-product-catalog」というメッセージを送信すると、次のような商品のカルーセルが表示されます。

4639da46bcc5230c.png

[Add item(商品を追加)] をタップしても、エージェントが返信の候補からポストバック データをエコーする以外、実際には何も起こりません。次のセクションでは、商品カタログを使用して商品が追加されるショッピング カートを作成します。

先ほど作成した商品カタログは、さまざまな方法で拡張できます。たとえば、さまざまなドリンクメニューや、ベジタリアン向けのメニューなどを表示できます。カルーセルや候補ワードを使用すると、ユーザーはメニュー オプションをドリルダウンして、目的の商品を見つけやすくなります。この Codelab の応用として、商品カタログ システムを拡張し、ユーザーがメニューでフードとは別にドリンクだけを表示できるようにしたり、ベジタリアン向けのメニューを指定できるようにしてみましょう。

4. ショッピング カート

Codelab のこのセクションでは、前のセクションをベースにしてショッピング カート機能を作成し、商品の在庫状況をユーザーが閲覧できるようにします。

ショッピング カートには多くの機能がありますが、その中でも重要な機能として、カートへの商品の追加や削除、カート内の各商品の数の管理、カート内の商品の確認などが挙げられます。

ショッピング カートの状態を管理するには、ウェブ アプリケーションでデータを保持する必要があります。テストとデプロイについて理解しやすいように、Google Cloud Platform で Google Datastore を使用してデータを保持します。会話 ID はユーザーと店舗の間で変わらないため、この ID を使用してユーザーをショッピング カートの商品に関連付けることができます。

まずは Google Datastore に接続し、会話 ID が出現したらその ID を保持することから始めましょう。

Datastore と接続する

ユーザーが商品を追加 / 削除するなど、ショッピング カートに対してなんらかの操作が実行されるたびに Google Datastore に接続します。このクライアント ライブラリを使用して Google Datastore を操作する方法については、公式ドキュメントをご覧ください。

次のスニペットは、ショッピング カートを更新する関数を定義しています。この関数は conversation_idmessage を入力として使用します。message には、ユーザー アクションを記述した JSON が格納されます。これはすでに、商品カタログを表示するカルーセルに組み込まれています。この関数は、Google Datastore クライアントを作成し、直ちに ShoppingCart エンティティを取得します。ここで重要になるのが会話 ID です。

次の関数を views.py ファイルにコピーします。次のセクションでは、この関数を拡張します。

bonjourmeal-codelab/step-2/bopis/views.py

from google.oauth2 import service_account
from google.cloud import datastore

def update_shopping_cart(conversation_id, message):
        credentials = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_LOCATION)

        client = datastore.Client(credentials=credentials)
        key = client.key('ShoppingCart', conversation_id)
        entity = datastore.Entity(key=key)
        result = client.get(key)
        
        # TODO: Add logic to add and remove items from cart
        
        entity.update(result)
        client.put(entity)

この関数を拡張して、カートに商品を追加しましょう。

カートに商品を追加する

ユーザーが商品カルーセルから [Add item(商品を追加)] 候補アクションをタップすると、そのアクションを記述した JSON が postbackData に格納されます。JSON 辞書には「action」と「item_name」の 2 つのキーが含まれており、この JSON 辞書が Webhook に送信されます。「item_name」フィールドは、inventory.json 内の商品に関連付けられた一意の識別子です。

カート コマンドとカートの商品がメッセージから解析されたら、条件ステートメントを記述して商品を追加します。ここで考慮すべきエッジケースとしては、これまで Datastore で会話 ID が確認されていない場合や、ショッピング カートで初めてこの商品を受け取る場合などが考えられます。以下は、上で定義した update_shopping_cart 機能の拡張機能です。この変更により、Google Datastore に保持されている商品がショッピング カートに追加されます。

以下のスニペットは、views.py に追加した、前出の関数の拡張関数です。別のスニペットを追加することも、このスニペットをコピーして update_shopping_cart 関数の既存のバージョンに置き換えることもできます。

bonjourmeal-codelab/step-2bopis/views.py

def update_shopping_cart(conversation_id, message):
    credentials = service_account.Credentials.from_service_account_file(
      SERVICE_ACCOUNT_LOCATION)
    inventory = get_inventory_data()

    cart_request = json.loads(message)
    cart_cmd = cart_request["action"]
    cart_item = cart_request["item_name"]

    item_name = inventory['food'][int(cart_item)]['name']

    client = datastore.Client(credentials=credentials)
    key = client.key('ShoppingCart', conversation_id)
    entity = datastore.Entity(key=key)
    result = client.get(key)

    if result is None:
        if cart_cmd == CMD_ADD_ITEM:
            entity.update({
                item_name: 1
            })

    else:
        if cart_cmd == CMD_ADD_ITEM:
            if result.get(item_name) is None:
                result[item_name] = 1
            else:
                result[item_name] = result[item_name] + 1

        entity.update(result)
    client.put(entity)

後でこの関数を拡張して、CMD_DEL_ITEM で定義した文字列「del-item」が cart_cmd に含まれているシナリオをサポートするようにします。

まとめ

route_message 関数で plumbling を追加し、カートに商品を追加するメッセージを受信した場合に update_shopping_cart 関数が呼び出されるようにします。また、Codelab 全体で使用する規則に沿って、商品を追加するための定数を定義する必要もあります。

bonjourmeal-codelab/step-2bopis/views.py

...

CMD_DEL_ITEM = 'del-item'

...

def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATEGORY:
        send_product_catalog(conversation_id)
    elif CMD_ADD_ITEM in normalized_message or CMD_DEL_ITEM in normalized_message:
       update_shopping_cart(conversation_id, message)
    else:
        echo_message(message, conversation_id)

...

これで、ショッピング カートに商品を追加できるようになりました。Google App Engine に変更をデプロイすると、ショッピング カードの変更が GCP Console の Google Datastore ダッシュボードに反映されます。以下の、Google Datastore Console のスクリーンショットをご覧ください。会話 ID にちなんで名付けられたエンティティが 1 つあり、その後に、ショッピング カート内の商品の在庫と数量の関係が表示されています。

619dc18a8136ea69.png

次のセクションでは、ショッピング カート内の商品を一覧表示する方法を構築します。ショッピング カートの確認メカニズムによって、カート内のすべての商品とそれらの数量、カートから商品を削除するオプションが表示されます。

カート内の商品を確認する

ショッピング カートの状態を確認して、削除できる商品を把握するには、ショッピング カート内の商品を一覧表示します。

まず「ショッピング カートはこちら」のような親しみやすいメッセージを送信してから、「1 つ削除」や「1 つ追加」といった関連する返信の候補を表示するリッチカード カルーセルを含むメッセージを送信します。リッチカード カルーセルには、カートに保存されている商品の数量も表示されます。

実際に関数を記述する前に注意すべき点があります。ショッピング カートに 1 種類の商品しかない場合は、それをカルーセルとしてレンダリングすることはできません。リッチカード カルーセルには 2 つ以上のカードが含まれている必要があります。一方、カートに商品が 1 つもない場合は、「カートが空です」という簡単なメッセージを表示します。

このことを念頭に置いて、send_shopping_cart という関数を定義しましょう。この関数は Google Datastore に接続し、会話 ID に基づいて ShoppingCart エンティティをリクエストします。エンティティが取得されたら、get_inventory_data 関数を呼び出し、リッチカード カルーセルを使用してショッピング カートの状態を報告します。また、商品の ID を名前で取得する必要もあります。関数を宣言して Google Datastore 内を調べ、値を特定します。カルーセルの作成時に、返信の候補を商品 ID で関連付けて、商品の削除 / 追加ができます。以下のスニペットでは、これらすべての処理を行います。コードを views.py の任意の場所にコピーします。

bonjourmeal-codelab/step-2/bopis/views.py

...
def get_id_by_product_name(product_name):
  inventory = get_inventory_data()
  for item in inventory['food']:
    if item['name'] == product_name:
      return int(item['id'])
  return False


def send_shopping_cart(conversation_id):
  credentials = service_account.Credentials.from_service_account_file(
      SERVICE_ACCOUNT_LOCATION)

  # Retrieve the inventory data
  inventory = get_inventory_data()

  # Pull the data from Google Datastore
  client = datastore.Client(credentials=credentials)
  key = client.key('ShoppingCart', conversation_id)
  result = client.get(key)

  shopping_cart_suggestions = [
      BusinessMessagesSuggestion(
          reply=BusinessMessagesSuggestedReply(
              text='See total price', postbackData='show-cart-price')),
      BusinessMessagesSuggestion(
          reply=BusinessMessagesSuggestedReply(
              text='Empty the cart', postbackData='empty-cart')),
      BusinessMessagesSuggestion(
          reply=BusinessMessagesSuggestedReply(
              text='See the menu', postbackData=CMD_SHOW_PRODUCT_CATALOG)),
  ]

  if result is None or len(result.items()) == 0:
    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        text='There are no items in your shopping cart.',
        suggestions=shopping_cart_suggestions)

    send_message(message_obj, conversation_id)
  elif len(result.items()) == 1:

    for product_name, quantity in result.items():
      product_id = get_id_by_product_name(product_name)

      fallback_text = ('You have one type of item in the shopping cart')

      rich_card = BusinessMessagesRichCard(
          standaloneCard=BusinessMessagesStandaloneCard(
              cardContent=BusinessMessagesCardContent(
                  title=product_name,
                  description=f'{quantity} in cart.',
                  suggestions=[
                      BusinessMessagesSuggestion(
                          reply=BusinessMessagesSuggestedReply(
                              text='Remove one',
                              postbackData='{'+f'"action":"{CMD_DEL_ITEM}","item_name":"{product_id}"'+'}'))
                  ],
                  media=BusinessMessagesMedia(
                      height=BusinessMessagesMedia.HeightValueValuesEnum.MEDIUM,
                      contentInfo=BusinessMessagesContentInfo(
                          fileUrl=inventory['food'][product_id]
                          ['image_url'],
                          forceRefresh=False)))))

      message_obj = BusinessMessagesMessage(
          messageId=str(uuid.uuid4().int),
          representative=BOT_REPRESENTATIVE,
          richCard=rich_card,
          suggestions=shopping_cart_suggestions,
          fallback=fallback_text)

      send_message(message_obj, conversation_id)
  else:
    cart_carousel_items = []

    # Iterate through the cart and generate a carousel of items
    for product_name, quantity in result.items():
      product_id = get_id_by_product_name(product_name)

      cart_carousel_items.append(
          BusinessMessagesCardContent(
              title=product_name,
              description=f'{quantity} in cart.',
              suggestions=[
                  BusinessMessagesSuggestion(
                      reply=BusinessMessagesSuggestedReply(
                          text='Remove one',
                          postbackData='{'+f'"action":"{CMD_DEL_ITEM}","item_name":"{product_id}"'+'}'))
              ],
              media=BusinessMessagesMedia(
                  height=BusinessMessagesMedia.HeightValueValuesEnum.MEDIUM,
                  contentInfo=BusinessMessagesContentInfo(
                      fileUrl=inventory['food'][product_id]
                      ['image_url'],
                      forceRefresh=False))))

    rich_card = BusinessMessagesRichCard(
        carouselCard=BusinessMessagesCarouselCard(
            cardContents=cart_carousel_items,
            cardWidth=BusinessMessagesCarouselCard.CardWidthValueValuesEnum
            .MEDIUM))

    fallback_text = ''

    # Construct a fallback text for devices that do not support carousels
    for card_content in rich_card.carouselCard.cardContents:
      fallback_text += (
          card_content.title + '\n\n' + card_content.description + '\n\n' +
          card_content.media.contentInfo.fileUrl +
          '\n---------------------------------------------\n\n')

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        richCard=rich_card,
        suggestions=shopping_cart_suggestions,
        fallback=fallback_text,
    )

    send_message(message_obj, conversation_id)

...

views.py の先頭で CMD_SHOW_CART が定義されていることを確認し、ユーザーが「show-cart」を含むメッセージを送信した場合は send_shopping_cart を呼び出します。

bonjourmeal-codelab/step-2/bopis/views.py

...
def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATEGORY:
        send_product_catalog(conversation_id)
    elif CMD_ADD_ITEM in normalized_message or CMD_DEL_ITEM in normalized_message:
        update_shopping_cart(conversation_id, message)
    elif normalized_message == CMD_SHOW_CART:
        send_shopping_cart(conversation_id)
    else:
        echo_message(message, conversation_id)
...

34801776a97056ac.png

send_shopping_cart 関数に導入したロジックに基づき、「show-cart」と入力されると、カートが空であることを示すメッセージ、カート内の 1 つの商品を示すリッチカード、あるいは複数の商品を示すカードのカルーセルのいずれかが取得されます。さらに、「See total price(合計金額を表示)」、「Empty the cart(カートを空にする)」、「See the menu(メニューを表示)」という 3 つの返信候補も用意されています。

上記のコード変更をデプロイして、追加した商品をショッピング カードで追跡できること、上のスクリーンショットに示すように、会話サーフェスからカートの内容を確認できることをテストします。変更を追加する step-2 ディレクトリでこのコマンドを直接実行すれば、変更をデプロイできます。

$ gcloud app deploy

カートから商品を削除する機能を構築したら、次のセクションでは「See total price(合計金額を表示)」機能を構築します。関数 get_cart_price は、Datastore 内のデータと Inventory.json ファイルを相互参照してショッピング カートの合計金額を導き出す点において、「See shopping cart(カートを表示)」機能と動作が似ています。これは、支払い機能を統合する、Codelab の次のパートで役立ちます。

カートから商品を削除する

最後に、カートから商品を削除する機能を導入して、ショッピング カートの構築を完了します。既存の update_shopping_cart 関数を、次のスニペットに置き換えます。

bonjourmeal-codelab/step-2/bopis/views.py を使用します。

def update_shopping_cart(conversation_id, message):
    credentials = service_account.Credentials.from_service_account_file(
      SERVICE_ACCOUNT_LOCATION)
    inventory = get_inventory_data()

    cart_request = json.loads(message)
    cart_cmd = cart_request["action"]
    cart_item = cart_request["item_name"]

    item_name = inventory['food'][int(cart_item)]['name']


    client = datastore.Client(credentials=credentials)
    key = client.key('ShoppingCart', conversation_id)
    entity = datastore.Entity(key=key)
    result = client.get(key)

    if result is None:
        if cart_cmd == CMD_ADD_ITEM:
            entity.update({
                item_name: 1
            })
        elif cart_cmd == CMD_DEL_ITEM:
            # The user is trying to delete an item from an empty cart. Pass and skip
            pass

    else:
        if cart_cmd == CMD_ADD_ITEM:
            if result.get(item_name) is None:
                result[item_name] = 1
            else:
                result[item_name] = result[item_name] + 1

        elif cart_cmd == CMD_DEL_ITEM:
            if result.get(item_name) is None:
                # The user is trying to remove an item that's no in the shopping cart. Pass and skip
                pass
            elif result[item_name] - 1 > 0:
                result[item_name] = result[item_name] - 1
            else:
                del result[item_name]

        entity.update(result)
    client.put(entity)

確認メッセージを送信する

ユーザーが商品をカートに追加したら、商品が追加されてリクエストが処理されたことを伝える確認メッセージを送信する必要があります。これにより、今後の流れを伝え、会話をスムーズに進めることもできます。

update_shopping_cart 関数を拡張し、商品が追加または削除されたことを伝えるメッセージを会話 ID に送信して、ショッピング カートの内容の確認やメニューの再表示を提案できるようにしましょう。

bonjourmeal-codelab/step-2/bopis/views.py

def update_shopping_cart(conversation_id, message):

     # No changes to the function, except appending the following logic
     ...
   
    if cart_cmd == CMD_ADD_ITEM:
        message = 'Great! You\'ve added an item to the cart.'
    else:
        message = 'You\'ve removed an item from the cart.'

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        text=message,
        suggestions=[
            BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='Review shopping cart',
                postbackData=CMD_SHOW_CART)
            ),
            BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='See menu again',
                postbackData=CMD_SHOW_PRODUCT_CATALOG)
            ),
            ])
    send_message(message_obj, conversation_id)

905a1f3d89893ba0.png

このように変更されました。カートへの商品の追加や削除、カート内の商品の確認を行える、フル機能を備えたショッピング カート。

この時点で、ショッピング カートの機能をビジネス メッセージの会話で確認したい場合は、アプリケーションをデプロイしてエージェントとやり取りしてみます。これを行うには、step-2 ディレクトリで次のコマンドを実行します。

$ gcloud app deploy

5. 支払いの準備をする

本シリーズの次のパートで決済代行業者と統合するための準備として、ショッピング カート内の商品の価格を取得する方法が必要になります。そこで、価格を取得する関数を作成しましょう。つまり、Google Datastore でショッピング カートのデータを相互参照して在庫から各商品の価格を取得し、その価格に、カート内の各商品の数量を掛けて価格を取得します。

bonjourmeal-codelab/step-2/bopis/views.py

...
def get_cart_price(conversation_id):
    # Pull the data from Google Datastore
    credentials = service_account.Credentials.from_service_account_file(
    SERVICE_ACCOUNT_LOCATION)
    client = datastore.Client(credentials=credentials)
    key = client.key('ShoppingCart', conversation_id)
    entity = datastore.Entity(key=key)
    result = client.get(key)

    # Retrieve the inventory data
    inventory = get_inventory_data()
   
    # Start off with a total of 0 before adding up the total
    total_price = 0

    if len(result.items()) != 0:
      for product_name, quantity in result.items():
        total_price = total_price + float(
            inventory['food'][get_id_by_product_name(product_name)]['price']) * int(quantity)

    return total_price

...

最後に、その関数を使用してユーザーにメッセージを送信します。

bonjourmeal-codelab/step-2/bopis/views.py

...

def send_shopping_cart_total_price(conversation_id):
    cart_price = get_cart_price(conversation_id)

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        suggestions=[],
        text=f'Your cart\'s total price is ${cart_price}.')

    send_message(message_obj, conversation_id)
...

すべてをまとめるために、route_message 関数と定数を更新して、上記のロジックをトリガーします。

bonjourmeal-codelab/step-2/bopis/views.py

...
CMD_GET_CART_PRICE = 'show-cart-price'
...
def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATEGORY:
        send_product_catalog(conversation_id)
    elif CMD_ADD_ITEM in normalized_message or CMD_DEL_ITEM in normalized_message:
        update_shopping_cart(conversation_id, message)
    elif normalized_message == CMD_SHOW_CART:
        send_shopping_cart(conversation_id)
    elif normalized_message == CMD_GET_CART_PRICE:
        send_shopping_cart_total_price(conversation_id)
    else:
        echo_message(message, conversation_id)
...

上記のロジックで実現できる内容をスクリーンショットで示します。

8feacf94ed0ac6c4.png

Codelab の次のパートで行う、支払い処理業者との統合の準備ができたら、get_cart_price 関数を呼び出して支払い処理業者にデータを送信し、支払いフローを開始します。

すでに述べたように、アプリケーションをデプロイしてエージェントとやり取りしてみて、このショッピング カートの機能をビジネス メッセージの会話で試すことができます。

$ gcloud app deploy

6. 完了

これで、ビジネス メッセージ内にショッピング カートを作成できました。

この Codelab では、ショッピング カート全体を空にする機能については扱っていません。必要に応じて、アプリケーションを拡張して「Empty the cart(カートを空にする)」機能を実行してみてください。このソリューションは、クローンを作成したソースコードのステップ 3 で利用できます。

この後のセクションでは、外部の支払い処理業者と統合して、ユーザーがお客様のブランドとの支払いトランザクションを完了できるようにする予定です。

使いやすいショッピング カートとは

会話形式の使いやすいショッピング カートは、モバイルアプリや実店舗と何ら変わりません。商品の追加、削除、カート内の商品の価格計算は、この Codelab で確認した機能のほんの一部です。実世界のショッピング カートと違う点は、商品を追加 / 削除する際に、いつでもカート内のすべての商品の価格を確認できることです。このような価値の高い機能によって、秀逸した会話型コマースを実現できます。

次のステップ

準備が整ったら、以下のトピックをご覧になり、ビジネス メッセージで行える複雑な操作についてご確認ください。

リファレンス ドキュメント