스트리밍 API를 사용하여 멀티모달 주문 환경 빌드

이 가이드는 FoodOrderingService.BidiProcessOrder RPC 메서드를 사용하여 음식 주문 환경을 빌드하는 엔지니어를 위한 안내 및 권장사항을 제공합니다. 이 실시간 양방향 스트리밍 API는 음식 주문 AI 에이전트의 핵심으로, 모바일 앱, 음성 어시스턴트, 드라이브 스루, 키오스크와 같은 다양한 애플리케이션에서 대화형 주문 접수를 지원합니다.

BidiProcessOrder 개요

BidiProcessOrder 메서드는 클라이언트 애플리케이션과 음식 주문 AI 에이전트 간에 지속적인 양방향 통신 채널을 설정합니다. 표준 단항 요청 및 응답 RPC와 달리 이 스트리밍 접근 방식은 다음을 지원합니다.

  • 지연 시간이 짧은 상호작용: 반복되는 HTTP 요청의 오버헤드 없이 지속적인 정보 교환
  • 멀티모달 입력: 음성 주문을 위한 오디오 스트림, 텍스트 입력, 클라이언트 측 이벤트 처리
  • 실시간 응답: 에이전트는 대화가 진행됨에 따라 오디오, 텍스트, 주문 업데이트, 기타 신호를 다시 보낼 수 있습니다.

BidiProcessOrder는 REST를 사용하여 호출할 수 없습니다. 통합은 연결 지향 프로토콜을 사용해야 합니다.

  • gRPC (권장): 양방향 스트리밍을 위한 강력하고 효율적인 프레임워크를 제공합니다.
  • WebSocket: 프로그래밍 언어 또는 네트워크 제약으로 인해 gRPC가 적합하지 않은 클라이언트 또는 환경에 적합합니다.

자세한 유형 정의는 BidiProcessOrder API 참조 를 확인하세요. WebSocket 통합은 이러한 유형의 JSON 표현 을 사용합니다. WebSocket 섹션에 설명된 대로 말이죠.

기본 요건

BidiProcessOrder와 통합하기 전에 다음을 수행하세요.

  1. API 사용 설정: 프로젝트에서 음식 주문 AI 에이전트 API가 사용 설정되어 있는지 확인합니다. Google Cloud bash gcloud services enable foodorderingaiagent.googleapis.com --project=PROJECT_ID

  2. 인증: 인증에 설명된 대로 인증 접근 방식을 결정하고 필요한 서비스 계정 및 IAM 역할을 설정합니다.

  3. 메뉴 수집: 유효한 메뉴 를 수집하고 Store와 연결해야 합니다. 자세한 내용은 메뉴 데이터 통합을 참고하세요.

인증

BidiProcessOrder RPC에 안전하게 연결하려면 애플리케이션이 서비스 계정을 사용하여 인증해야 합니다. Google Cloud

1. 서비스 계정 구성

  • 서비스 계정 만들기: 프로젝트에서 애플리케이션이 음식 주문 AI 에이전트 API에 인증하는 데 사용할 서비스 계정을 만듭니다. Google Cloud 서비스 계정 만들기 및 관리 를 참고하세요.
  • IAM 역할 부여: 이 서비스 계정에 필요한 IAM 역할을 부여합니다. BidiProcessOrder를 호출하는 데 필요한 기본 역할은 다음과 같습니다.

    • 음식 주문 에이전트 사용자 (roles/foodorderingaiagent.agentUser): 서비스 계정이 주문 서비스에 연결하고 세션을 처리할 수 있도록 허용합니다.

    콘솔 또는 gcloud을 사용하여 이 역할을 부여할 수 있습니다. bash gcloud projects add-iam-policy-binding PROJECT_ID \ --member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \ --role="roles/foodorderingaiagent.agentUser" Google Cloud

2. 애플리케이션 인증 흐름

정확한 인증 흐름은 애플리케이션 아키텍처, 특히 클라이언트 애플리케이션 (예: 모바일 앱, 키오스크 소프트웨어)이 직접 연결되는지 아니면 자체 백엔드를 통해 연결되는지에 따라 다릅니다.

일반적인 시나리오: 소비자 대상 클라이언트 애플리케이션 인증

이는 모바일 또는 웹 애플리케이션의 일반적인 패턴입니다.

  1. Client-to-YourAuth: 최종 사용자 클라이언트 앱 (모바일, 웹)이 자체 기존 사용자 인증 시스템 (Firebase 인증, 자체 OAuth 서버 등)으로 인증합니다.
  2. 토큰 교환: 클라이언트 앱은 사용자를 인증한 후 자체 제어하는 보안 백엔드 서비스 (예: 'API 토큰 서비스')에서 수명이 짧은 토큰을 요청합니다.
  3. 액세스 토큰 생성: 백엔드 서비스는 1단계에서 구성된 서비스 계정 주 구성원의 사용자 인증 정보를 사용하여 표준 OAuth 2.0 액세스 토큰을 https://www.googleapis.com/auth/cloud-platform 범위에 대해 생성합니다. Google Cloud 인증 클라이언트 라이브러리를 사용하여 이 작업을 수행할 수 있습니다. Google Cloud

    • 보안: 이러한 토큰을 생성하는 데 사용되는 서비스 계정 키 또는 사용자 인증 정보는 백엔드에 안전하게 저장되고 관리되어야 합니다. 서비스 계정 비공개 키를 최종 사용자 클라이언트 애플리케이션에 직접 노출하지 마세요. 서비스 계정 키 관리 권장사항 을 참고하세요.
  4. 클라이언트 토큰: 백엔드 서비스는 생성된 Google 액세스 토큰을 클라이언트 앱에 반환합니다.

  5. API 호출: 클라이언트 앱은 이 Google 액세스 토큰을 사용하여 BidiProcessOrder RPC에 대한 gRPC 또는 WebSocket 연결을 인증합니다.

3. 토큰 사용

  • gRPC: Google gRPC 클라이언트 라이브러리는 일반적으로 서비스 계정 사용자 인증 정보가 제공되면 토큰 새로고침 및 호출 메타데이터 포함을 처리합니다.
  • WebSocket (브라우저 외): Authorization: Bearer TOKEN 헤더에 토큰을 포함합니다.
  • WebSocket (브라우저): WebSocket 섹션에 설명된 대로 직접 브라우저 WebSocket 연결은 승인 헤더를 사용할 수 없습니다. 클라이언트 연결을 인증하려면 Google Cloud서버 측 스트리밍 프록시가 필요합니다.

API에 연결

gRPC 클라이언트 라이브러리 또는 WebSocket 연결을 사용하여 스트림을 설정할 수 있습니다.

gRPC

gRPC를 사용하는 것이 좋습니다. BidiProcessOrder API 참조를 기반으로 하는 선택한 언어 (예: Node.js)의 클라이언트 라이브러리를 사용합니다.

기본 단계는 다음과 같습니다.

  1. 음식 주문 AI 에이전트 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로 전송되는 메시지 (예: Config, AudioInput, TextInput, EventInput)는 websocket.TextMessage로 전송되는 BidiProcessOrderRequest 프로토의 JSON 표현이어야 합니다.
  • 서버-클라이언트: API(BidiProcessOrderResponse)에서 수신된 메시지는 websocket.BinaryMessage로 전송되지만 이러한 바이너리 메시지의 콘텐츠는 JSON 페이로드입니다.
  • 바이너리 데이터: JSON 페이로드 내의 바이너리 데이터 (예: AudioInputcustomerAudio, AgentAudioagentAudio)는 base64로 인코딩되어야 합니다.

Node.js WebSocket 예

다음은 ws 라이브러리를 사용하여 Node.js에서 WebSockets를 사용하여 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. 시작 (구성 메시지)

  • 연결이 설정되면 클라이언트가 전송하는 첫 번째 메시지 BidiProcessOrderRequestConfig 메시지가 포함되어야 합니다.
  • 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: 원시 오디오 데이터 (일반적으로 16000Hz의 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 현재 전체 상태를 포함합니다. 에이전트가 업데이트할 때마다 이를 사용하여 애플리케이션의 주문 표현을 업데이트합니다. 일반적으로 판매시점관리 시스템과 같은 주문 상태 정보의 사용자 인터페이스 또는 기록 시스템이 업데이트됩니다.
    • 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, 16000Hz 샘플링 레이트
  • 오디오 청크에는 일반적으로 WAV 파일 앞에 오는 오디오 헤더가 포함되지 않습니다.
  • 에코 제거가 사용 설정된 드라이브 스루 시나리오(Configenable_echo_cancellation)의 경우 customer_audiocrew_audio를 모두 제공합니다.

UpdatedOrderState

  • 이 메시지는 전송될 때마다 주문의 전체 상태를 제공합니다. 수신된 Order 메시지의 콘텐츠로 주문의 로컬 캐시를 바꿉니다.
  • Order 항목 및 수정자 내에서 custom_integration_attributes를 사용하여 Order 콘텐츠를 애플리케이션의 기록 시스템 내에서 상응하는 항목에 매핑합니다.

InterruptionSignal

  • 수신 시 AgentAudio 재생을 즉시 중지하고 버퍼링된 에이전트 오디오를 지웁니다. 이렇게 하면 사용자가 에이전트의 음성을 중단할 때 자연스러운 대화 흐름이 보장됩니다.

EndSession

  • EndType (예: DRIVE_OFF, AGENT_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

이 샘플을 사용해 보기 전에 Node.js 음식 주문 AI 에이전트 빠른 시작: 클라이언트 라이브러리 사용의 설정 안내를 따르세요.

음식 주문 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.'}});
}