使用串流 API 建構多模態訂購體驗

本指南提供相關操作說明和最佳做法,協助工程師使用 FoodOrderingService.BidiProcessOrder RPC 方法建構餐點訂購體驗。這項雙向即時串流 API 是訂餐 AI Agent 的核心,可在行動應用程式、語音助理、得來速和自助服務機等各種應用程式中,動態地以對話方式接單。

BidiProcessOrder 總覽

BidiProcessOrder 方法會在用戶端應用程式和訂餐 AI Agent 之間建立持續的雙向通訊管道。與標準一元要求和回應 RPC 不同,這種串流方法可實現下列功能:

  • 低延遲互動:持續交換資訊,無須重複發出 HTTP 要求,因此不會產生額外負擔。
  • 多模態輸入:處理音訊串流 (用於語音訂購)、文字輸入和用戶端事件。
  • 即時回覆:代理可以在對話過程中傳回音訊、文字、訂單更新和其他信號。

BidiProcessOrder 無法透過 REST 呼叫。整合項目必須使用連線導向通訊協定:

  • gRPC (建議):提供強大且有效率的雙向串流架構。
  • WebSocket:適用於因程式設計語言或網路限制,而不適合使用 gRPC 的用戶端或環境。

如需詳細的型別定義,請參閱 BidiProcessOrder API 參考資料。WebSocket 整合會使用這些型別的 JSON 表示法,詳情請參閱 WebSocket 專區

必要條件

BidiProcessOrder 整合前,請先完成下列步驟:

  1. 啟用 API:確認專案已啟用訂餐 AI 代理 API。 Google Cloudbash gcloud services enable foodorderingaiagent.googleapis.com --project=PROJECT_ID

  2. 驗證:決定驗證方法,並設定所有必要的服務帳戶和 IAM 角色,如「驗證」一文所述。

  3. 菜單擷取:必須擷取有效的菜單,並與 Store 建立關聯。詳情請參閱「整合菜單資料」。

驗證

如要安全地連線至 BidiProcessOrder RPC,應用程式必須使用 Google Cloud 服務帳戶進行驗證。

1. 設定服務帳戶

  • 建立服務帳戶:在 Google Cloud 專案中,建立應用程式將用來向訂餐 AI 代理 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 驗證、您自己的 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 標頭。您需要伺服器端串流 Proxy,才能驗證用戶端與 Google Cloud的連線。

連結至 API

您可以使用 gRPC 用戶端程式庫或 WebSocket 連線建立串流。

gRPC

建議使用 gRPC。您將使用所選語言 (例如 Node.js) 的用戶端程式庫,這些程式庫是以 BidiProcessOrder API 參考資料為基礎。

基本步驟包括:

  1. 建立 gRPC 通道,連線至訂餐 AI Agent API 端點 (例如 foodorderingaiagent.googleapis.com)。
  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 連線,網址路徑為:

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

  • LOCATION:例如 us

必要標頭:

  • AuthorizationBearer TOKEN - 其中 TOKEN 是為服務帳戶取得的 OAuth 2.0 存取權杖。

訊息格式:

  • 用戶端到伺服器:傳送至 API 的訊息 (例如 ConfigAudioInputTextInputEventInput) 必須是 BidiProcessOrderRequest proto 的 JSON 表示法,並以 websocket.TextMessage 形式傳送。
  • 伺服器到用戶端:從 API (BidiProcessOrderResponse) 收到的訊息會以 websocket.BinaryMessage 形式傳送,但這些二進位訊息的內容是 JSON 酬載。
  • 二進位資料:JSON 酬載中的二進位資料 (例如 customerAudio in AudioInputagentAudio in AgentAudio) 必須採用 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:用戶端產生的專屬工作階段 ID。格式: projects/PROJECT/locations/LOCATION/sessions/SESSION_ID
      • storeStore 的資源名稱。格式: 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: 原始音訊資料 (通常為 16000 Hz 的 16 位元線性 PCM,不含標頭)。 用於語音互動。
    • 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,取樣率為 16000 Hz。
  • 音訊區塊包含通常位於 WAV 檔案開頭的音訊標頭。
  • 如果啟用消除回音功能 (enable_echo_cancellation 位於 Config),請提供 customer_audiocrew_audio

UpdatedOrderState

  • 每次傳送這則訊息時,都會提供訂單的完整狀態。 以收到的 Order 訊息內容,取代訂單的任何本機快取。
  • Order 項目和修飾符中使用 custom_integration_attributes,將 Order 內容對應至應用程式記錄系統中的對等實體。

InterruptionSignal

  • 收到後,請立即停止播放所有「AgentAudio」,並清除所有緩衝的代理程式音訊。確保使用者打斷代理程式的語音時,對話流程仍自然流暢。

EndSession

  • 檢查「EndType」(例如 DRIVE_OFFAGENT_ESCALATION)。
  • 應用程式應正常關閉連線,並適當轉移使用者 (例如,在 AGENT_ESCALATION 的情況下通知人工主管,或轉移至訂單確認狀態)。

最佳做法

  • 以非同步方式處理訊息:使用執行緒或非封鎖 I/O 同時傳送要求及處理傳入的回應,盡量減少延遲。
  • 重新連線邏輯:如果發生網路問題,請實作穩健的重新連線邏輯,並記得傳送具有相同工作階段 ID 的初始 Config 訊息,嘗試恢復連線。
  • 錯誤處理:監控串流是否有錯誤。gRPC 和 WebSocket 程式庫提供偵測串流關閉或傳輸錯誤的機制。記錄這些事件並妥善處理。
  • 音訊緩衝:請謹慎管理音訊緩衝區,並視需要實作緩衝區,確保 AgentAudio 順暢播放,並及時傳送 AudioInput。決定緩衝區配置時,請仔細考量延遲時間與畫質之間的取捨。
  • 工作階段 ID 管理:確保每個不同的訂單/對話都有專屬的工作階段 ID。
  • 資源管理:工作階段完成或發生無法復原的錯誤時,請關閉串流並釋出資源。
  • 逾時:串流本身可以長時間存在 (預設最多 15 分鐘),但如有需要,請考慮特定狀態的應用程式層級逾時。

整合流程範例 (概念)

  1. 用戶端應用程式 (例如行動應用程式) 會發起訂單。
  2. 建立與 BidiProcessOrder 的 gRPC/WebSocket 連線。
  3. 傳送 BidiProcessOrderRequest,並附上 Config (工作階段 ID、商店 ID)。
  4. 接收並播放初始 AgentAudio (例如歡迎訊息)。
  5. 使用者說話:擷取音訊,並在 AudioInput 訊息中串流播放。
  6. 接收 SpeechRecognition (顯示轉錄稿)、AgentAudio (播放回覆) 和可能的 UpdatedOrderState (更新 UI 購物車)。
  7. 如果使用者中斷,請接收 InterruptionSignal 並停止播放。
  8. 繼續交換音訊或文字輸入內容和服務專員回覆。
  9. 使用者確認訂單:代理傳送最終 UpdatedOrderState
  10. 服務專員傳送 EndSession:客戶關閉串流,並使用最後一次 UpdatedOrderState 的資料,在 POS 系統中完成訂單。

端對端範例

上述操作說明將串流概念逐一分解,以下是完整的端對端整合流程。

Node.js

在試用這個範例之前,請先按照「使用用戶端程式庫的訂餐 AI 代理快速入門導覽課程」中的 Node.js 設定說明操作。

如要向訂餐 AI 代理進行驗證,請設定應用程式預設憑證。詳情請參閱「為本機開發環境設定驗證機制」。

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.'}});
}