ストリーミング API を使用してマルチモーダル注文エクスペリエンスを構築する

このガイドでは、FoodOrderingService.BidiProcessOrder RPC メソッドを使用して食品注文エクスペリエンスを構築するエンジニア向けの手順とベスト プラクティスについて説明します。このリアルタイムの双方向ストリーミング API は、デリバリー&テイクアウト AI エージェントの中核であり、モバイルアプリ、音声アシスタント、ドライブスルー、キオスクなどのさまざまなアプリケーションで動的な会話型の注文受付を可能にします。

BidiProcessOrder の概要

BidiProcessOrder メソッドは、クライアント アプリケーションとデリバリー&テイクアウト AI Agent の間に永続的な双方向通信チャネルを確立します。標準の単項リクエストとレスポンスの RPC とは異なり、このストリーミング アプローチでは次のことが可能です。

  • 低レイテンシのインタラクション: HTTP リクエストの繰り返しによるオーバーヘッドなしで情報を継続的に交換します。
  • マルチモーダル入力: 音声ストリーム(音声注文用)、テキスト入力、クライアントサイド イベントの処理。
  • リアルタイムの応答: エージェントは、会話の進行に合わせて、音声、テキスト、注文の更新などのシグナルを送信できます。

BidiProcessOrder は REST を使用して呼び出すことはできません。統合では、接続指向のプロトコルを使用する必要があります。

  • gRPC(推奨): 双方向ストリーミング用の堅牢で効率的なフレームワークを提供します。
  • WebSocket: プログラミング言語やネットワークの制約により gRPC が適していないクライアントや環境に適しています。

詳細な型定義については、BidiProcessOrder API リファレンスをご覧ください。WebSocket 統合では、WebSocket セクションで説明するように、これらの型の JSON 表現を使用します。

前提条件

BidiProcessOrder と統合する前に:

  1. API を有効にする: Google Cloudプロジェクトでデリバリー&テイクアウト AI Agent API が有効になっていることを確認します。 bash gcloud services enable foodorderingaiagent.googleapis.com --project=PROJECT_ID

  2. 認証: 認証の説明に従って、認証方法を決定し、必要なサービス アカウントと IAM ロールを設定します。

  3. メニューの取り込み: 有効なメニューを取り込み、Store に関連付ける必要があります。詳しくは、メニューデータの統合をご覧ください。

認証

BidiProcessOrder RPC に安全に接続するには、アプリケーションが Google Cloud サービス アカウントを使用して認証する必要があります。

1. サービス アカウントを構成する

  • サービス アカウントを作成する: Google Cloud プロジェクトで、アプリケーションがデリバリー&テイクアウト AI Agent API への認証に使用するサービス アカウントを作成します。サービス アカウントの作成と管理をご覧ください。
  • IAM ロールを付与する: このサービス アカウントに必要な IAM ロールを付与します。BidiProcessOrder を呼び出すために必要な主なロールは次のとおりです。

    • デリバリー&テイクアウト エージェント ユーザーroles/foodorderingaiagent.agentUser): サービス アカウントが注文サービスに接続してセッションを処理できるようにします。

    このロールは、 Google Cloud コンソールまたは gcloud を使用して付与できます。bash gcloud projects add-iam-policy-binding PROJECT_ID \ --member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \ --role="roles/foodorderingaiagent.agentUser"

2. アプリケーション認証フロー

正確な認証フローは、アプリケーション アーキテクチャ、特にクライアント アプリケーション(モバイルアプリ、キオスク ソフトウェアなど)が直接接続するか、独自のバックエンド経由で接続するかによって異なります。

一般的なシナリオ: 一般ユーザー向けのクライアント アプリケーションの認証

これは、モバイル アプリケーションまたはウェブ アプリケーションの一般的なパターンです。

  1. Client-to-YourAuth: エンドユーザー クライアント アプリ(モバイル、ウェブ)は、既存のユーザー認証システム(Firebase Authentication、独自の OAuth サーバーなど)で認証します。
  2. トークン交換: クライアント アプリは、ユーザーを認証した後、が制御する安全なバックエンド サービス(「API トークン サービス」など)から有効期間の短いトークンをリクエストします。
  3. アクセス トークンの生成: バックエンド サービスは、ステップ 1 で構成された Google Cloud サービス アカウント プリンシパルの認証情報を使用して、https://www.googleapis.com/auth/cloud-platform スコープの標準 OAuth 2.0 アクセス トークンを生成します。これは、Google Cloud 認証クライアント ライブラリを使用して行うことができます。

    • セキュリティ: これらのトークンの生成に使用されるサービス アカウント キーまたは認証情報は、バックエンドで安全に保存および管理する必要があります。サービス アカウントの秘密鍵をエンドユーザーのクライアント アプリケーションに直接公開しないでください。サービス アカウント キーを管理するためのベスト プラクティスをご覧ください。
  4. トークンからクライアントへ: バックエンド サービスが、生成された Google アクセス トークンをクライアント アプリに返します。

  5. API 呼び出し: クライアント アプリは、この Google アクセス トークンを使用して、BidiProcessOrder RPC への gRPC または WebSocket 接続を認証します。

3. トークンの使用

  • gRPC: Google gRPC クライアント ライブラリは通常、サービス アカウントの認証情報が提供されると、トークンの更新と呼び出しメタデータへの追加を処理します。
  • WebSocket(ブラウザ以外): Authorization: Bearer TOKEN ヘッダーにトークンを含めます。
  • WebSocket(ブラウザ): WebSocket セクションで説明したように、ブラウザの WebSocket 接続では Authorization ヘッダーを使用できません。クライアントの Google Cloudへの接続を認証するには、サーバーサイド ストリーミング プロキシが必要です。

API への接続

ストリームは、gRPC クライアント ライブラリまたは WebSocket 接続を使用して確立できます。

gRPC

gRPC を使用することをおすすめします。BidiProcessOrder API リファレンスに基づく、選択した言語(Node.js など)のクライアント ライブラリを使用します。

基本的な手順は次のとおりです。

  1. デリバリー&テイクアウト AI Agent API エンドポイント(foodorderingaiagent.googleapis.com など)への gRPC チャネルを作成します。
  2. FoodOrderingService のクライアント スタブを取得します。
  3. BidiProcessOrder メソッドを呼び出します。このメソッドは、リクエストの送信とレスポンスの受信の両方を行うストリーム オブジェクトを返します。
  4. ユースケースに応じてビジネス ロジックを実装します。このビジネス ロジックは、次の処理を同時に行います。
    • エンドユーザーからの音声、テキスト、イベント入力を送信します。
    • 音声、テキスト、イベントなど、エージェントからのメッセージを処理します。

Node.js


const {FoodOrderingServiceClient} = require('@google-cloud/foodorderingaiagent');

const client = new FoodOrderingServiceClient();

// The stream is initialized immediately. You can now write commands and attach listeners.

const stream = client.bidiProcessOrder();

WebSocket

WebSocket 接続の場合、URL パスは次のようになります。

wss://foodorderingaiagent.googleapis.com/ws/google.cloud.foodorderingaiagent.v1beta.FoodOrderingService/BidiProcessOrder/locations/LOCATION

  • LOCATION: 例: us

必須ヘッダー:

  • Authorization: Bearer TOKEN - TOKEN は、サービス アカウント用に取得された OAuth 2.0 アクセス トークンです。

メッセージ形式:

  • クライアントからサーバー: API に送信されるメッセージ(ConfigAudioInputTextInputEventInput など)は、BidiProcessOrderRequest proto の JSON 表現でなければならず、websocket.TextMessage として送信されます。
  • サーバーからクライアント: API(BidiProcessOrderResponse)から受信したメッセージは websocket.BinaryMessage として送信されますが、これらのバイナリ メッセージの内容は JSON ペイロードです。
  • バイナリデータ: JSON ペイロード内のバイナリデータ(AudioInputcustomerAudioAgentAudioagentAudio など)は base64 でエンコードする必要があります。

Node.js WebSocket の例

Node.js で ws ライブラリを使用して WebSocket で API に接続し、操作する例を次に示します。

const WebSocket = require('ws');

// Replace with your actual values
const location = 'LOCATION';
const projectId = 'PROJECT_ID';
const sessionId = 'SESSION_ID';
const brandId = 'BRAND_ID';
const storeId = 'STORE_ID';
const token = 'OAUTH_TOKEN';

const wsUrl = `wss://foodorderingaiagent.googleapis.com/ws/google.cloud.foodorderingaiagent.v1beta.FoodOrderingService/BidiProcessOrder/locations/${location}`;

const ws = new WebSocket(wsUrl, {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

ws.on('open', () => {
  console.log('Connected to WebSocket');

  // 1. Send the required initial Config message
  const configRequest = {
    config: {
      session: `projects/${projectId}/locations/${location}/sessions/${sessionId}`,
      store: `projects/${projectId}/locations/${location}/brands/${brandId}/stores/${storeId}`
    }
  };

  // Client-to-server messages are sent as TextMessage
  ws.send(JSON.stringify(configRequest));
  console.log('Sent Config message');
});

ws.on('message', (data, isBinary) => {
  // The documentation specifies that server-to-client messages
  // are sent as BinaryMessage containing a JSON payload.
  if (isBinary) {
    try {
      const response = JSON.parse(data.toString('utf8'));
      console.log('Received response:', response);

      if (response.agentText) {
        console.log(`Agent: ${response.agentText.text}`);
      }

      if (response.agentAudio) {
        const audioBytes = Buffer.from(response.agentAudio.agentAudio, 'base64');
        console.log(`Received ${audioBytes.length} bytes of agent audio.`);
        // Play or process the audio bytes here
      }

      if (response.endSession) {
        console.log('Session ended by agent.');
        ws.close();
      }
    } catch (e) {
      console.error('Failed to parse JSON response:', e);
    }
  }
});

ws.on('close', () => {
  console.log('Connection closed');
});

セッションのライフサイクル

BidiProcessOrder の呼び出しごとにセッションが開始されます。ストリームが開いている間、セッションはアクティブな状態を維持します。

1. 開始(構成メッセージ)

  • 接続を確立すると、クライアントから送信される最初のメッセージは、Config メッセージを含む BidiProcessOrderRequest でなければなりません。
  • Config の必須フィールド:
    • session: クライアントによって生成された一意のセッション識別子。形式: projects/PROJECT/locations/LOCATION/sessions/SESSION_ID
      • store: Store のリソース名。形式: projects/PROJECT/locations/LOCATION/brands/BRAND/stores/STORE
        • エージェントは store を使用して、適切なメニューと構成を読み込みます。

Node.js

// Send the first message containing Config
stream.write({
  config: {
    session: client.sessionPath(projectId, location, sessionId),
    store: client.storePath(projectId, location, brandId, storeId),
  }
});

2. 入力の送信

  • 最初の Config の後、クライアントは次のいずれかの入力を含む BidiProcessOrderRequest メッセージのストリームを送信できます。
    • AudioInput: Raw 音声データ(通常は 16 ビットのリニア PCM、16, 000 Hz、ヘッダーなし)。 音声操作に使用されます。
    • TextInput: ユーザーからのテキスト メッセージ。
    • EventInput: DriveOffEvent(車両が出発したときのドライブスルーのユースケース)、CrewInterjectionEvent(会話中に人間が注文受付の役割を引き継いだ場合)、OrderStateUpdateEvent(クライアントサイドで注文が変更された場合(タッチ インターフェースの使用など))などのイベントのシグナル。

Node.js

// Stream user inputs over the active connection
stream.write({textInput: {text: 'Hi, I\'d like to order a cheeseburger.'}});

3. 回答の受信

  • 同時に、エージェントは BidiProcessOrderResponse メッセージのストリームを返送します。クライアントは、oneof response フィールド内のさまざまなレスポンス タイプを処理する準備をする必要があります。
    • AgentAudio: 音声インタラクションに使用される、ユーザーに再生される合成音声のバイト数。
    • AgentText: エージェントの応答のテキスト バージョン。
    • SpeechRecognition: 認識されたユーザーの音声の文字起こし。
    • UpdatedOrderState: エージェントが更新するたびに、お客様の Order の現在の状態全体が含まれます。アプリケーションの注文表現を更新するために使用します。通常、これにより、ユーザー インターフェースまたは注文ステータス情報の記録システム(POS システムなど)が更新されます。
    • InterruptionSignal: ユーザーがエージェントの発話を中断したことを示します。クライアントは、送信中の AgentAudio の再生を直ちに停止する必要があります。
    • AgentEvent: クライアント アクションを必要とする特別なイベント(RestartOrder など)。
    • SuggestedOptions: ユーザーが次に選択する可能性のあるコンテキストに関連するオプションを提供します。画面に表示するのに便利です。
    • EndSession: エージェントによってセッションが終了したことを示します(注文完了、ユーザーのドライブオフ、エージェントによるエスカレーションなど)。

Node.js

// Attach event listeners to handle responses sequentially
stream.on('data', (response) => {
  if (response.agentAudio) {
    console.log(`Received ${response.agentAudio.agentAudio.length} bytes of agent audio.`);
  } else if (response.agentText) {
    console.log(`Agent: ${response.agentText.text}`);
  } else if (response.speechRecognition) {
    console.log(`Recognized User Speech: ${response.speechRecognition.transcript}`);
  } else if (response.updatedOrderState) {
    console.log('Order updated.');
  } else if (response.interruptionSignal) {
    console.log('User interrupted the agent. Stop playing audio!');
  } else if (response.endSession) {
    console.log(`Session ended. Type: ${response.endSession.type}, Reason: ${response.endSession.reason}`);
    stream.end();
  }
});

stream.on('error', (err) => {
  console.error('Stream error:', err);
});

4. ストリームを閉じる

  • ストリームは、クライアントまたはサーバーによって閉じることができます。通常、サーバーは EndSession メッセージを使用して会話の終了を通知します。クライアントは、このメッセージを受信したらストリームを閉じる必要があります。

特定のメッセージ タイプの処理

以降のセクションでは、BidiProcessOrder の呼び出し時にクライアントが受け取る特定のレスポンス タイプを処理する方法について説明します。

AudioInput

  • 音声が利用可能になると、チャンク単位でストリーミングします。
  • 形式: 16 ビット リニア PCM、16, 000 Hz サンプルレート。
  • 音声チャンクには、通常 WAV ファイルの接頭辞となる音声ヘッダーは含まれません。
  • エコー キャンセラが有効になっているドライブスルー シナリオ(Configenable_echo_cancellation)では、customer_audiocrew_audio の両方を提供します。

UpdatedOrderState

  • このメッセージは、送信されるたびに注文の完全な状態を提供します。注文のローカル キャッシュを、受信した Order メッセージの内容に置き換えます。
  • Order アイテムと修飾子内の custom_integration_attributes を使用して、Order コンテンツをアプリケーションのシステム オブ レコード内の同等のエンティティにマッピングします。

InterruptionSignal

  • 受信したら、直ちに AgentAudio の再生を停止し、バッファリングされたエージェントの音声をクリアします。これにより、ユーザーがエージェントの発言を中断した場合でも、自然な会話の流れを維持できます。

EndSession

  • EndTypeDRIVE_OFFAGENT_ESCALATION など)を確認します。
  • アプリケーションは、接続を正常に閉じ、ユーザーを適切に移行する必要があります(AGENT_ESCALATION の場合は人間の監督者に通知する、注文確認状態に移行するなど)。

ベスト プラクティス

  • メッセージを非同期で処理する: スレッドまたはノンブロッキング I/O を使用してリクエストを同時に送信し、受信したレスポンスを処理することで、レイテンシを最小限に抑えます。
  • 再接続ロジック: ネットワークの問題が発生した場合に、堅牢な再接続ロジックを実装します。再開を試みるために、同じセッション ID を含む最初の Config メッセージを送信することを忘れないでください。
  • エラー処理: エラーがないかストリームをモニタリングします。gRPC ライブラリと WebSocket ライブラリには、ストリームの終了や転送エラーを検出するメカニズムが用意されています。これらのイベントをログに記録し、適切に処理します。
  • 音声バッファリング: AgentAudio のスムーズな再生と AudioInput のタイムリーな配信を確保するため、必要に応じてバッファリングを実装し、音声バッファを慎重に管理します。バッファリング スキームを決定する際は、レイテンシと再生画質のトレードオフを慎重に検討してください。
  • セッション ID の管理: セッション ID が注文や会話ごとに一意であることを確認します。
  • リソース管理: セッションが完了したとき、または回復不能なエラーが発生したときに、ストリームを閉じてリソースを解放します。
  • タイムアウト: ストリーム自体は長時間存続できます(デフォルトでは最大 15 分)が、必要に応じて特定の状態のアプリケーション レベルのタイムアウトを検討してください。

統合フローの例(概念)

  1. クライアント アプリ(モバイルアプリなど)が注文を開始します。
  2. BidiProcessOrder への gRPC/WebSocket 接続を確立します。
  3. Config(セッション ID、店舗 ID)を含む BidiProcessOrderRequest を送信します。
  4. 最初の AgentAudio(ウェルカム メッセージなど)を受信して再生します。
  5. ユーザーが話す: 音声をキャプチャし、AudioInput メッセージでストリーミングします。
  6. SpeechRecognition(文字起こしを表示)、AgentAudio(レスポンスを再生)、場合によっては UpdatedOrderState(UI カートを更新)を受け取ります。
  7. ユーザーが中断した場合は、InterruptionSignal を受け取り、再生を停止します。
  8. 音声入力またはテキスト入力とエージェントのレスポンスのやり取りを続けます。
  9. お客様が注文を確認: エージェントが最終的な UpdatedOrderState を送信します。
  10. エージェントが EndSession を送信: クライアントがストリームを閉じ、最後の UpdatedOrderState のデータを使用して POS システムで注文を確定します。

エンドツーエンドの例

上記の手順ではストリーミングのコンセプトを段階的に説明しましたが、完全なエンドツーエンドの統合フローは次のようになります。

Node.js

このサンプルを試す前に、デリバリー&テイクアウト AI エージェント クイックスタート: クライアント ライブラリの使用にある Node.js の設定手順を完了してください。

デリバリー&テイクアウト AI Agent に対する認証を行うには、アプリケーションのデフォルト認証情報を設定します。詳細については、ローカル開発環境の認証の設定をご覧ください。

const {FoodOrderingServiceClient} = require('@google-cloud/foodorderingaiagent');

async function bidiProcessOrderSample(projectId, location, brand, store, sessionId) {
  const client = new FoodOrderingServiceClient();

  // Create the resource names
  const sessionPath = client.sessionPath(projectId, location, sessionId);
  const storePath = client.storePath(projectId, location, brand, store);

  // Initialize the stream using gRPC. See the WebSocket section for the equivalent WebSocket implementation.
  const stream = client.bidiProcessOrder();

  // Attach event listeners to handle responses sequentially
  stream.on('data', (response) => {
    if (response.agentAudio) {
      console.log(`Received ${response.agentAudio.agentAudio.length} bytes of agent audio.`);
    } else if (response.agentText) {
      console.log(`Agent: ${response.agentText.text}`);
    } else if (response.speechRecognition) {
      console.log(`Recognized User Speech: ${response.speechRecognition.transcript}`);
    } else if (response.updatedOrderState) {
      console.log('Order updated.');
    } else if (response.interruptionSignal) {
      console.log('User interrupted the agent. Stop playing audio!');
    } else if (response.endSession) {
      console.log(`Session ended. Type: ${response.endSession.type}, Reason: ${response.endSession.reason}`);
      stream.end();
    }
  });

  stream.on('error', (err) => {
    console.error('Stream error:', err);
  });

  // 1. Send the first message containing Config
  stream.write({
    config: {
      session: sessionPath,
      store: storePath,
    }
  });

  // 2. Stream user inputs over the active connection
  stream.write({textInput: {text: 'Hi, I\'d like to order a cheeseburger.'}});
}