Créer une expérience de commande multimodale à l'aide de l'API Streaming

Ce guide fournit des instructions et des bonnes pratiques pour les ingénieurs qui créent des expériences de commande de nourriture avec la méthode RPC FoodOrderingService.BidiProcessOrder. Cette API de streaming bidirectionnel en temps réel est au cœur de l'agent d'IA de commande de nourriture. Elle permet de prendre des commandes de manière dynamique et conversationnelle dans diverses applications telles que les applications mobiles, les assistants vocaux, les drive-in et les kiosques.

Présentation de BidiProcessOrder

La méthode BidiProcessOrder établit un canal de communication bidirectionnel persistant entre votre application cliente et l'agent d'IA de commande de repas. Contrairement aux RPC de requête et de réponse unaires standards, cette approche de streaming permet :

  • Interaction à faible latence : échange continu d'informations sans la surcharge de requêtes HTTP répétées.
  • Entrée multimodale : gestion des flux audio (pour les commandes vocales), des entrées de texte et des événements côté client.
  • Réponses en temps réel : l'agent peut renvoyer des signaux audio, textuels, des informations sur les commandes et d'autres signaux au fur et à mesure de la conversation.

BidiProcessOrder ne peut pas être appelé à l'aide de REST. Les intégrations doivent utiliser un protocole orienté connexion :

  • gRPC (recommandé) : fournit un framework robuste et efficace pour le streaming bidirectionnel.
  • WebSocket : convient aux clients ou aux environnements où gRPC n'est pas adapté en raison de contraintes liées au langage de programmation ou au réseau.

Consultez la documentation de référence de l'API BidiProcessOrder pour obtenir des définitions détaillées des types. Les intégrations WebSocket utilisent des représentations JSON de ces types, comme décrit dans la section WebSocket.

Prérequis

Avant d'intégrer BidiProcessOrder :

  1. Activez l'API : assurez-vous que l'API Food Ordering AI Agent est activée dans votre projet Google Cloud. bash gcloud services enable foodorderingaiagent.googleapis.com --project=PROJECT_ID

  2. Authentification : choisissez votre approche d'authentification et configurez les comptes de service et les rôles IAM nécessaires, comme décrit dans Authentification.

  3. Ingestion de menus : un menu valide doit être ingéré et associé à un Store. Pour en savoir plus, consultez Intégrer les données de menu.

Authentification

Pour vous connecter de manière sécurisée au RPC BidiProcessOrder, votre application doit s'authentifier à l'aide d'un compte de service Google Cloud .

1. Configurer un compte de service

  • Créez un compte de service : dans votre projet Google Cloud , créez un compte de service que votre application utilisera pour s'authentifier auprès de l'API Food Ordering AI Agent. Consultez Créer et gérer des comptes de service.
  • Attribuez des rôles IAM : attribuez les rôles IAM nécessaires à ce compte de service. Le rôle principal requis pour appeler BidiProcessOrder est le suivant :

    • Utilisateur de l'agent de commande de repas (roles/foodorderingaiagent.agentUser) : permet au compte de service de se connecter au service de commande et de traiter les sessions.

    Vous pouvez attribuer ce rôle à l'aide de la console Google Cloud ou de gcloud : bash gcloud projects add-iam-policy-binding PROJECT_ID \ --member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \ --role="roles/foodorderingaiagent.agentUser"

2. Flux d'authentification de l'application

Le flux d'authentification exact dépend de l'architecture de votre application, en particulier si l'application cliente (par exemple, une application mobile ou un logiciel de borne) se connecte directement ou via votre propre backend.

Scénario courant : authentifier une application cliente destinée aux consommateurs

Voici un schéma typique pour les applications mobiles ou Web :

  1. Client-to-YourAuth: : l'application cliente de l'utilisateur final (mobile, Web) s'authentifie auprès de votre système d'authentification des utilisateurs existant (Firebase Authentication, votre propre serveur OAuth, etc.).
  2. Échange de jetons : après avoir authentifié l'utilisateur, l'application cliente demande un jeton éphémère à un service de backend sécurisé que vous contrôlez (par exemple, un "service de jetons d'API").
  3. Génération de jetons d'accès : votre service de backend, à l'aide des identifiants du principal du compte de service Google Cloud configuré à l'étape 1, génère un jeton d'accès OAuth 2.0 standard pour le champ d'application https://www.googleapis.com/auth/cloud-platform. Pour ce faire, utilisez les bibliothèques clientes d'authentificationGoogle Cloud .

    • Sécurité : Les clés ou identifiants de compte de service utilisés pour générer ces jetons doivent être stockés et gérés de manière sécurisée sur votre backend. N'exposez jamais les clés privées de compte de service directement aux applications clientes des utilisateurs finaux. Consultez Bonnes pratiques pour gérer les clés de compte de service.
  4. Jeton vers le client : votre service de backend renvoie le jeton d'accès Google généré à l'application cliente.

  5. Appel d'API : l'application cliente utilise ce jeton d'accès Google pour authentifier sa connexion gRPC ou WebSocket au RPC BidiProcessOrder.

3. Utiliser le jeton

  • gRPC : les bibliothèques clientes Google gRPC gèrent généralement l'actualisation des jetons et leur inclusion dans les métadonnées d'appel lorsqu'elles reçoivent des identifiants de compte de service.
  • WebSocket (non lié au navigateur) : incluez le jeton dans l'en-tête Authorization: Bearer TOKEN.
  • WebSocket (navigateur) : comme indiqué dans la section WebSocket, les connexions WebSocket directes du navigateur ne peuvent pas utiliser d'en-têtes d'autorisation. Un proxy de streaming côté serveur est nécessaire pour authentifier la connexion de vos clients à Google Cloud.

Se connecter à l'API

Vous pouvez établir un flux à l'aide des bibliothèques clientes gRPC ou d'une connexion WebSocket.

gRPC

L'utilisation de gRPC est l'approche recommandée. Vous utiliserez les bibliothèques clientes pour la langue de votre choix (par exemple, Node.js), qui sont basées sur la documentation de référence de l'API BidiProcessOrder.

Les étapes de base sont les suivantes :

  1. Créez un canal gRPC vers le point de terminaison de l'API Food Ordering AI Agent (par exemple, foodorderingaiagent.googleapis.com).
  2. Obtenez un stub client pour FoodOrderingService.
  3. Appelez la méthode BidiProcessOrder, qui renvoie un objet de flux pour l'envoi de requêtes et la réception de réponses.
  4. Implémentez la logique métier en fonction de votre cas d'utilisation, qui, en même temps :
    • Envoie des entrées audio, textuelles et d'événement de l'utilisateur final.
    • Gère les messages de l'agent, y compris l'audio, le texte et les événements.

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

Pour les connexions WebSocket, le chemin d'URL est le suivant :

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

  • LOCATION : par exemple, us

En-têtes obligatoires :

  • Authorization : Bearer TOKEN, où TOKEN est un jeton d'accès OAuth 2.0 obtenu pour votre compte de service.

Format du message :

  • Client vers serveur : les messages envoyés à l'API (par exemple, Config, AudioInput, TextInput, EventInput) doivent être des représentations JSON du proto BidiProcessOrderRequest, envoyées en tant que websocket.TextMessage.
  • Serveur vers client : les messages reçus de l'API (BidiProcessOrderResponse) seront envoyés au format websocket.BinaryMessage, mais le contenu de ces messages binaires est une charge utile JSON.
  • Données binaires : les données binaires dans les charges utiles JSON (par exemple, customerAudio dans AudioInput, agentAudio dans AgentAudio) doivent être encodées en base64.

Exemple Node.js WebSocket

Voici un exemple de connexion et d'interaction avec l'API à l'aide de WebSockets dans Node.js avec la bibliothèque 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');
});

Cycle de vie des sessions

Chaque appel à BidiProcessOrder lance une session. La session reste active tant que le flux est ouvert.

1. Activation (message de configuration)

  • Une fois la connexion établie, le premier message envoyé par le client doit être un BidiProcessOrderRequest contenant le message Config.
  • Champs obligatoires dans Config :
    • session : identifiant de session unique généré par le client. Format : projects/PROJECT/locations/LOCATION/sessions/SESSION_ID.
      • store : nom de ressource de Store. Format : projects/PROJECT/locations/LOCATION/brands/BRAND/stores/STORE.
        • L'agent utilise store pour charger le menu et la configuration appropriés.

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. Envoyer des entrées

  • Après le Config initial, le client peut envoyer un flux de messages BidiProcessOrderRequest contenant l'une des entrées suivantes :
    • AudioInput : données audio brutes (généralement PCM linéaire 16 bits à 16 000 Hz, sans en-têtes). Utilisé pour les interactions vocales.
    • TextInput : messages de l'utilisateur.
    • EventInput : signaux pour des événements tels que DriveOffEvent (pour les cas d'utilisation au drive lorsque le véhicule quitte le point de vente), CrewInterjectionEvent (pour toute situation dans laquelle un humain reprend le rôle de prise de commande en cours de conversation) ou OrderStateUpdateEvent (si la commande est modifiée côté client, par exemple à l'aide d'une interface tactile).

Node.js

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

3. Recevoir des réponses

  • Parallèlement, l'agent renvoie un flux de messages BidiProcessOrderResponse. Votre client doit être prêt à gérer différents types de réponses dans le champ oneof response :
    • AgentAudio : octets audio synthétisés à lire à l'utilisateur, utilisés pour les interactions vocales.
    • AgentText : Version texte de la réponse de l'agent.
    • SpeechRecognition : transcription de la parole de l'utilisateur reconnue.
    • UpdatedOrderState : contient l'état actuel complet de Order du client chaque fois qu'il est mis à jour par l'agent. Utilisez-le pour mettre à jour la représentation de la commande de votre application. Cela devrait généralement entraîner une mise à jour d'une interface utilisateur ou d'un système d'enregistrement des informations sur l'état des commandes, comme un système de point de vente.
    • InterruptionSignal : indique que l'utilisateur a interrompu le discours de l'agent. Le client doit immédiatement cesser de lire tout AgentAudio sortant.
    • AgentEvent : Événements spéciaux, tels que RestartOrder, nécessitant une action du client.
    • SuggestedOptions : fournit des options contextuelles qu'un utilisateur peut sélectionner ensuite. Cette option est utile pour l'affichage à l'écran.
    • EndSession : indique que la session a été interrompue par l'agent (par exemple, commande terminée, départ de l'utilisateur ou escalade de l'agent).

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. Fermer le flux

  • Le flux peut être fermé par le client ou le serveur. En règle générale, le serveur signale la fin d'une conversation à l'aide d'un message EndSession. Le client doit fermer le flux lorsque ce message est reçu.

Gérer des types de messages spécifiques

Les sections suivantes décrivent comment gérer des types de réponses spécifiques que votre client recevra lors de l'appel de BidiProcessOrder.

AudioInput

  • Diffuser l'audio par blocs dès qu'il est disponible.
  • Format : PCM linéaire 16 bits, taux d'échantillonnage de 16 000 Hz.
  • Les blocs audio n'incluent pas les en-têtes audio qui précèdent généralement un fichier WAV.
  • Pour les scénarios de drive-thru avec annulation d'écho activée (enable_echo_cancellation dans Config), fournissez customer_audio et crew_audio.

UpdatedOrderState

  • Ce message fournit l'état complet de la commande chaque fois qu'il est envoyé. Remplacez tout cache local de la commande par le contenu du message Order reçu.
  • Utilisez custom_integration_attributes dans les éléments et modificateurs Order pour mapper le contenu Order dans des entités équivalentes au sein du système d'enregistrement de votre application.

InterruptionSignal

  • Dès réception, arrêtez immédiatement la lecture de tout AgentAudio et effacez tout audio d'agent mis en mémoire tampon. Cela permet de garantir un flux de conversation naturel lorsque l'utilisateur interrompt le discours de l'agent.

EndSession

  • Consultez le EndType (par exemple, DRIVE_OFF, AGENT_ESCALATION).
  • Votre application doit fermer la connexion de manière fluide et faire passer l'utilisateur à l'étape suivante de manière appropriée (par exemple, en informant un superviseur humain en cas de AGENT_ESCALATION ou en passant à un état de confirmation de commande).

Bonnes pratiques

  • Gérez les messages de manière asynchrone : minimisez la latence en utilisant des threads ou des E/S non bloquantes pour envoyer des requêtes et traiter les réponses entrantes simultanément.
  • Logique de reconnexion : implémentez une logique de reconnexion robuste en cas de problèmes réseau, en veillant à envoyer le message Config initial avec le même ID de session pour tenter de reprendre la connexion.
  • Gestion des erreurs : surveillez le flux pour détecter les erreurs. Les bibliothèques gRPC et WebSocket fournissent des mécanismes permettant de détecter la fermeture du flux ou les erreurs de transport. Enregistrez ces événements et gérez-les de manière optimale.
  • Mise en mémoire tampon de l'audio : gérez soigneusement les mémoires tampons audio, en implémentant la mise en mémoire tampon si nécessaire, pour assurer une lecture fluide de AgentAudio et une diffusion rapide de AudioInput. Lorsque vous choisissez votre schéma de mise en mémoire tampon, réfléchissez attentivement au compromis entre la latence et la qualité de lecture.
  • Gestion des ID de session : assurez-vous que les ID de session sont uniques pour chaque commande/conversation distincte.
  • Gestion des ressources : fermez les flux et libérez les ressources lorsque la session est terminée ou si des erreurs irrécupérables se produisent.
  • Délai d'inactivité : bien que le flux lui-même puisse être de longue durée (jusqu'à 15 minutes par défaut), envisagez des délais d'inactivité au niveau de l'application pour des états spécifiques si nécessaire.

Exemple de flux d'intégration (conceptuel)

  1. L'application cliente (application mobile, par exemple) initie une commande.
  2. Établissez une connexion gRPC/WebSocket à BidiProcessOrder.
  3. Envoyez BidiProcessOrderRequest avec Config (ID de session, ID de magasin).
  4. Recevez le AgentAudio initial (par exemple, un message de bienvenue) et l'écoutez.
  5. L'utilisateur parle : capturez l'audio et diffusez-le dans des messages AudioInput.
  6. Recevez SpeechRecognition (afficher la transcription), AgentAudio (lire la réponse) et potentiellement UpdatedOrderState (mettre à jour le panier de l'UI).
  7. Si l'utilisateur interrompt la lecture, recevez InterruptionSignal et arrêtez la lecture.
  8. Continuez à échanger des entrées audio ou textuelles et des réponses de l'agent.
  9. L'utilisateur confirme la commande : l'agent envoie le UpdatedOrderState final.
  10. L'agent envoie EndSession : le client ferme le flux et finalise la commande dans le système de point de vente à l'aide des données du dernier EndSession.UpdatedOrderState

Exemple de bout en bout

Les instructions ci-dessus décomposent les concepts de streaming étape par étape. Voici à quoi ressemble un flux d'intégration complet de bout en bout.

Node.js

Avant d'essayer cet exemple, suivez les instructions de configuration pour Node.js décrites dans le guide de démarrage rapide de l'agent d'IA de commande de repas à l'aide des bibliothèques clientes.

Pour vous authentifier auprès de l'agent d'IA Food Ordering, configurez les Identifiants par défaut de l'application. Pour en savoir plus, consultez Configurer l'authentification pour un environnement de développement local.

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