Creare un'esperienza di ordinazione multimodale utilizzando l'API di streaming

Questa guida fornisce istruzioni e best practice per gli ingegneri che creano esperienze di ordinazione di cibo con il metodo RPC FoodOrderingService.BidiProcessOrder. Questa API di streaming bidirezionale in tempo reale è il cuore di Food Ordering AI Agent e consente l'acquisizione di ordini dinamici e conversazionali in varie applicazioni, come app mobile, assistenti vocali, drive-through e chioschi.

Panoramica di BidiProcessOrder

Il metodo BidiProcessOrder stabilisce un canale di comunicazione bidirezionale persistente tra l'applicazione client e l'agente AI per l'ordinazione di cibo. A differenza delle RPC standard di richiesta e risposta unarie, questo approccio di streaming consente di:

  • Interazione a bassa latenza: scambio continuo di informazioni senza l'overhead delle richieste HTTP ripetute.
  • Input multimodale: gestione di stream audio (per l'ordinazione vocale), input di testo ed eventi lato client.
  • Risposte in tempo reale: l'agente può inviare audio, testo, aggiornamenti degli ordini e altri segnali man mano che la conversazione si svolge.

BidiProcessOrder non può essere chiamato utilizzando REST. Le integrazioni devono utilizzare un protocollo orientato alla connessione:

  • gRPC (consigliato): fornisce un framework robusto ed efficiente per lo streaming bidirezionale.
  • WebSocket: adatto a client o ambienti in cui gRPC non è adatto a causa di vincoli di linguaggio di programmazione o di rete.

Per definizioni dettagliate dei tipi, consulta il riferimento API BidiProcessOrder Reference. Le integrazioni WebSocket utilizzano rappresentazioni JSON di questi tipi, come descritto nella sezione WebSocket.

Prerequisiti

Prima di eseguire l'integrazione con BidiProcessOrder:

  1. Abilita l'API: assicurati che l'API Food Ordering AI Agent sia abilitata nel tuo Google Cloud progetto.bash gcloud services enable foodorderingaiagent.googleapis.com --project=PROJECT_ID

  2. Autenticazione: decidi l'approccio di autenticazione e configura tutti i service account e i ruoli IAM necessari, come descritto in Autenticazione.

  3. Importazione del menu: è necessario importare un menu valido e associarlo a un Store. Per maggiori dettagli, consulta Integrare i dati del menu.

Autenticazione

Per connettersi in modo sicuro alla RPC BidiProcessOrder, l'applicazione deve eseguire l'autenticazione utilizzando un Google Cloud service account.

1. Configura un service account

  • Crea un service account: nel tuo Google Cloud progetto, crea un service account che la tua applicazione utilizzerà per l'autenticazione all'API Agente AI per l'Ordinazione di cibo. Consulta la pagina Creazione e gestione dei service account.
  • Concedi ruoli IAM: concedi i ruoli IAM necessari a questo account di servizio. Il ruolo principale richiesto per chiamare BidiProcessOrder è:

    • Utente agente di ordinazione di cibo (roles/foodorderingaiagent.agentUser): consente al account di servizio di connettersi al servizio di ordinazione ed elaborare le sessioni.

    Puoi concedere questo ruolo utilizzando la Google Cloud console o gcloud: bash gcloud projects add-iam-policy-binding PROJECT_ID \ --member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \ --role="roles/foodorderingaiagent.agentUser"

2. Flusso di autenticazione dell'applicazione

Il flusso di autenticazione esatto dipende dall'architettura dell'applicazione, in particolare se l'applicazione client (ad es. app mobile, software per chioschi) si connette direttamente o tramite il tuo backend.

Scenario comune: autenticazione di un'applicazione client rivolta ai consumatori

Questo è un pattern tipico per le applicazioni web o mobile:

  1. Client-to-YourAuth: l'app client dell'utente finale (mobile, web) esegue l'autenticazione con il tuo sistema di autenticazione utente esistente (potrebbe essere l'autenticazione Firebase, il tuo server OAuth e così via).
  2. Scambio di token: dopo aver autenticato l'utente, l'app client richiede un token di breve durata da un servizio di backend sicuro che controlli (ad es. un "servizio token API").
  3. Generazione del token di accesso: il tuo servizio di backend, utilizzando le credenziali dell' entità del service account configurata nel passaggio 1, genera un token di accesso OAuth 2.0 standard per l' https://www.googleapis.com/auth/cloud-platform ambito. Google Cloud Questa operazione può essere eseguita utilizzando le Google Cloud librerie client di autenticazione.

    • Sicurezza: le chiavi o le credenziali del service account utilizzate per generare questi token devono essere archiviate e gestite in modo sicuro nel backend. Non esporre mai le chiavi private del account di servizio direttamente alle applicazioni client dell'utente finale. Consulta le best practice per la gestione delle chiavi account di servizio .
  4. Token al client: il tuo servizio di backend restituisce il token di accesso Google generato all'app client.

  5. Chiamata API: l'app client utilizza questo token di accesso Google per autenticare la connessione gRPC o WebSocket alla RPC BidiProcessOrder.

3. Utilizzo del token

  • gRPC: le librerie client gRPC di Google in genere gestiscono l'aggiornamento e l'inclusione dei token nei metadati delle chiamate quando vengono fornite le credenziali del account di servizio.
  • WebSocket (non browser): includi il token nell'intestazione Authorization: Bearer TOKEN.
  • WebSocket (browser): come indicato nella sezione WebSocket, le connessioni WebSocket dirette del browser non possono utilizzare le intestazioni di autorizzazione. È necessario un proxy di streaming lato server per autenticare la connessione dei client a Google Cloud.

Connessione all'API

Puoi stabilire uno stream utilizzando le librerie client gRPC o una connessione WebSocket.

gRPC

L'approccio consigliato è l'utilizzo di gRPC. Utilizzerai le librerie client per la tua lingua che preferisci (ad es. Node.js), basate sul riferimento API BidiProcessOrder.

I passaggi di base sono:

  1. Crea un canale gRPC per l'endpoint API Food Ordering AI Agent (ad es. foodorderingaiagent.googleapis.com).
  2. Ottieni uno stub client per FoodOrderingService.
  3. Richiama il metodo BidiProcessOrder, che restituisce un oggetto stream per l'invio di richieste e la ricezione di risposte.
  4. Implementa la logica di business in base al tuo caso d'uso, che contemporaneamente:
    • Invia input audio, di testo ed eventi dall'utente finale.
    • Gestisce i messaggi dell'agente, inclusi audio, testo ed eventi.

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

Per le connessioni WebSocket, il percorso dell'URL è:

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

  • LOCATION: ad es. us

Intestazioni obbligatorie:

  • Authorization: Bearer TOKEN - Dove TOKEN è un token di accesso OAuth 2.0 ottenuto per il tuo account di servizio.
consulta Autenticazione.

Formato dei messaggi:

  • Client-server: i messaggi inviati all'API (ad es. Config, AudioInput, TextInput, EventInput) devono essere rappresentazioni JSON del proto BidiProcessOrderRequest, inviate come websocket.TextMessage.
  • Server-client: i messaggi ricevuti dall'API (BidiProcessOrderResponse) verranno inviati come websocket.BinaryMessage, ma il contenuto di questi messaggi binari è un payload JSON.
  • Dati binari: i dati binari all'interno dei payload JSON (ad es. customerAudio in AudioInput, agentAudio in AgentAudio) devono essere codificati in base64.

Esempio di WebSocket Node.js

Ecco un esempio di come connettersi e interagire con l'API utilizzando WebSocket in Node.js con la libreria ws:

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

Ciclo di vita della sessione

Ogni chiamata a BidiProcessOrder avvia una sessione. La sessione rimane attiva finché lo stream è aperto.

1. Inizializzazione (messaggio di configurazione)

  • Dopo aver stabilito la connessione, il primo messaggio inviato dal client deve essere un BidiProcessOrderRequest contenente il Config messaggio.
  • Campi obbligatori in Config:
    • session: un identificatore di sessione univoco generato dal client. Formato: projects/PROJECT/locations/LOCATION/sessions/SESSION_ID.
      • store: il nome della risorsa dello Store. Formato: projects/PROJECT/locations/LOCATION/brands/BRAND/stores/STORE.
        • L'agente utilizza store per caricare il menu e la configurazione appropriati.

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. Invio di input

  • Dopo la Config iniziale, il client può inviare uno stream di messaggi BidiProcessOrderRequest contenenti uno dei seguenti input:
    • AudioInput: dati audio non elaborati (in genere PCM lineare a 16 bit a 16000 Hz, senza intestazioni). Utilizzato per le interazioni vocali.
    • TextInput: messaggi di testo dell'utente.
    • EventInput: segnali per eventi come DriveOffEvent (per i casi d'uso drive-through quando il veicolo parte), CrewInterjectionEvent (per qualsiasi situazione in cui un essere umano assume il ruolo di acquisizione dell'ordine a metà conversazione) o OrderStateUpdateEvent (se l'ordine viene modificato lato client, ad es. utilizzando un'interfaccia touch).

Node.js

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

3. Ricezione delle risposte

  • Contemporaneamente, l'agente invia uno stream di messaggi BidiProcessOrderResponse. Il client deve essere pronto a gestire vari tipi di risposta all'interno del campo oneof response:
    • AgentAudio: byte audio sintetizzati da riprodurre all'utente, utilizzati per le interazioni vocali.
    • AgentText: versione di testo della risposta dell'agente.
    • SpeechRecognition: trascrizione del discorso dell'utente riconosciuto.
    • UpdatedOrderState: Contiene lo stato corrente completo dell' Order ogni volta che viene aggiornato dall'agente. Utilizza questo campo per aggiornare la rappresentazione dell'ordine dell'applicazione. In genere, questa operazione comporta un aggiornamento di un'interfaccia utente o di un sistema di registrazione per le informazioni sullo stato dell'ordine, ad esempio un sistema point of sale.
    • InterruptionSignal: indica che l'utente ha interrotto il discorso dell'agente. Il client deve interrompere immediatamente la riproduzione di qualsiasi AgentAudio in uscita.
    • AgentEvent: eventi speciali, come RestartOrder, che richiedono l'intervento del client.
    • SuggestedOptions: fornisce opzioni contestualmente pertinenti che un utente potrebbe selezionare in seguito, utili per la visualizzazione su uno schermo.
    • EndSession: segnala che la sessione è stata terminata dall'agente (ad es. ordine completato, utente che ha lasciato il drive-through o escalation dell'agente).

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. Chiusura dello stream

  • Lo stream può essere chiuso dal client o dal server. In genere, il server segnala la fine di una conversazione utilizzando un messaggio EndSession. Il client deve chiudere lo stream quando riceve questo messaggio.

Gestione di tipi di messaggi specifici

Nelle sezioni seguenti viene descritto come gestire tipi di risposta specifici che il client riceverà quando chiama BidiProcessOrder.

AudioInput

  • Trasmetti in streaming l'audio in blocchi man mano che diventa disponibile.
  • Formato: PCM lineare a 16 bit, frequenza di campionamento di 16000 Hz.
  • I blocchi audio non includono le intestazioni audio che in genere precedono un file WAV.
  • Per gli scenari drive-through con cancellazione eco attivata (enable_echo_cancellation in Config), fornisci sia customer_audio che crew_audio.

UpdatedOrderState

  • Questo messaggio fornisce lo stato completo dell'ordine ogni volta che viene inviato. Sostituisci qualsiasi cache locale dell'ordine con i contenuti del messaggio Order ricevuto.
  • Utilizza custom_integration_attributes all'interno degli articoli e dei modificatori Order per mappare i contenuti Order in entità equivalenti all'interno del sistema di registrazione dell'applicazione.

InterruptionSignal

  • Al momento della ricezione, interrompi immediatamente la riproduzione di qualsiasi AgentAudio e cancella l'audio dell'agente memorizzato nel buffer. In questo modo si garantisce un flusso conversazionale naturale quando l'utente interrompe il discorso dell'agente.

EndSession

  • Controlla EndType (ad es. DRIVE_OFF, AGENT_ESCALATION).
  • L'applicazione deve chiudere correttamente la connessione e passare l'utente in modo appropriato (ad es. notificare a un supervisore umano in caso di AGENT_ESCALATION o passare a uno stato di conferma dell'ordine).

Best practice

  • Gestisci i messaggi in modo asincrono: riduci al minimo la latenza utilizzando thread o I/O non bloccanti per inviare contemporaneamente le richieste ed elaborare le risposte in entrata.
  • Logica di riconnessione: implementa una logica di riconnessione robusta in caso di problemi di rete, ricordando di inviare il messaggio Config iniziale con lo stesso ID sessione per tentare di riprendere la sessione.
  • Gestione degli errori: monitora lo stream per rilevare eventuali errori. Le librerie gRPC e WebSocket forniscono meccanismi per rilevare la chiusura dello stream o gli errori di trasporto. Registra questi eventi e gestiscili in modo appropriato.
  • Buffer audio: gestisci attentamente i buffer audio, implementando il buffering, se necessario, per garantire una riproduzione fluida di AgentAudio e la consegna tempestiva di AudioInput. Valuta attentamente il compromesso tra latenza e qualità di riproduzione quando decidi lo schema di buffering.
  • Gestione degli ID sessione: assicurati che gli ID sessione siano univoci per ogni ordine/conversazione distinta.
  • Gestione delle risorse: chiudi gli stream e rilascia le risorse al termine della sessione o se si verificano errori non recuperabili.
  • Timeout: anche se lo stream stesso può essere di lunga durata (fino a 15 minuti per impostazione predefinita), valuta la possibilità di utilizzare timeout a livello di applicazione per stati specifici, se necessario.

Flusso di integrazione di esempio (concettuale)

  1. L'app client (ad es. app mobile) avvia un ordine.
  2. Stabilisci la connessione gRPC/WebSocket a BidiProcessOrder.
  3. Invia BidiProcessOrderRequest con Config (ID sessione, ID negozio).
  4. Ricevi l'audio iniziale dell'agente (AgentAudio, ad es. messaggio di benvenuto) e riproducilo.
  5. L'utente parla: acquisisci l'audio e trasmettilo in streaming nei messaggi AudioInput.
  6. Ricevi SpeechRecognition (visualizza la trascrizione), AgentAudio (riproduci la risposta) e, potenzialmente, UpdatedOrderState (aggiorna il carrello dell'interfaccia utente).
  7. Se l'utente interrompe, ricevi InterruptionSignal e interrompi la riproduzione.
  8. Continua lo scambio di input audio o di testo e le risposte dell'agente.
  9. L'utente conferma l'ordine: l'agente invia l'ultimo UpdatedOrderState.
  10. L'agente invia EndSession: il client chiude lo stream e finalizza l'ordine nel sistema POS utilizzando i dati dell'ultimo UpdatedOrderState.

Esempio end-to-end

Sebbene le istruzioni riportate sopra suddividano i concetti di streaming passo dopo passo, ecco come appare un flusso di integrazione end-to-end completo.

Node.js

Prima di provare questo esempio, segui le istruzioni di configurazione Node.js nella guida rapida di Ordinazione di cibo AI Agent per l'utilizzo delle librerie client.

Per eseguire l'autenticazione in Agente AI per l'ordinazione di cibo, configura le Credenziali predefinite dell'applicazione. Per saperne di più, consulta Configura l'autenticazione per un ambiente di sviluppo locale.

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