BYOVA + Google CX Agent Studio (GECX) Setup Guide

This guide explains how to connect Webex Contact Center (WxCC) BYOVA to an agent built in CX Agent Studio (Gemini Enterprise for Customer Experience) using the GECXConnector and the CES BidiRunSession API.

Architecture

Caller -> WxCC -> BYOVA Gateway (gRPC) -> GECXConnector -> CES BidiRunSession -> CX Agent Studio

The connector sends WxCC caller audio to Google as it arrives and forwards CES 8 kHz mu-law output as BYOVA CHUNK responses. Normal short CES frames stream immediately. A guarded speech gate suppresses only an anomalously long, low-energy prefix and retains bounded pre-roll before prompt speech. Each agent turn ends with exactly one FINAL when CES marks it complete; terminal turns place TRANSFER_TO_AGENT or SESSION_END on that final response. Autonomous CES no-input prompts are pushed directly to the active WxCC stream. Barge-in is disabled by default and can be enabled only for those prompts after the interruption path is validated end to end.

Prerequisites

1. Google Cloud and CX Agent Studio

Enable APIs and IAM

  1. Enable the Gemini Enterprise for Customer Experience / CES API in your GCP project.
  2. Create a service account with role roles/ces.client.
  3. Download a JSON key (development) or configure Application Default Credentials.
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
  --member="serviceAccount:YOUR_SA@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/ces.client"

Identify your app (deployment is optional)

  1. Open CX Agent Studio and open your agent app.
  2. The app URL contains everything you need:
https://ces.cloud.google.com/projects/PROJECT_ID/locations/REGION/apps/APPLICATION_ID
  1. project_id, location, and application_id are all you need. A published deployment is optional — if you omit deployment_id, the connector runs sessions against the app’s root (draft) agent, which is convenient for testing and iterating. Provide deployment_id only to pin to a specific published version.

Endpoint note: the streaming BidiRunSession RPC is served from the regional CES runtime endpoint ces.<location>.rep.googleapis.com (e.g. ces.us.rep.googleapis.com), not the global ces.googleapis.com. The connector derives this automatically from location; override with api_endpoint if required.

2. Gateway configuration

Install dependencies

python -m venv venv
source venv/bin/activate     # macOS/Linux
# venv\Scripts\activate     # Windows
pip install -r requirements.txt

Configure the connector

Add a block to config/config.yaml (see config/gecx_example.yaml):

gecx_connector:
  type: "gecx_connector"
  class: "GECXConnector"
  module: "connectors.gecx_connector"
  config:
    project_id: "YOUR_PROJECT_ID"
    location: "us"
    application_id: "YOUR_APPLICATION_ID"
    # deployment_id: "YOUR_DEPLOYMENT_ID"   # optional; omit for root/draft agent
    language_code: "en-US"
    input_sample_rate_hertz: 8000
    input_audio_encoding: "MULAW"
    # Recommended quality path. Use 8000/MULAW for direct CES mu-law.
    output_sample_rate_hertz: 24000
    output_audio_encoding: "LINEAR16"
    suppress_long_leading_audio: true
    output_leading_audio_min_ms: 5000
    output_speech_rms_threshold: 200
    output_speech_start_frames: 2
    output_speech_preroll_ms: 100
    initial_message: "Hello"
    enable_partial_responses: true
    # Keep prompt interruption disabled until the barge-in path is validated.
    barge_in_enabled: false
    force_input_format: "wxcc"
    turn_response_timeout_seconds: 30
    endpointing_silence_ms: 2000
    input_preroll_ms: 500
    input_holdback_ms: 250
    input_pause_preroll_ms: 250
    input_stream_chunk_ms: 100
    input_queue_max_chunks: 20
    input_queue_put_timeout_ms: 50
    # Omit auth entirely to use Application Default Credentials (recommended on
    # Google Cloud; the runtime service account needs roles/ces.client).
    # service_account_key: "C:/path/to/ces-service-account.json"
    agents:
      - "My GECX Agent"

The agents list entry is the name WxCC uses when selecting a virtual agent in ListVirtualAgents.

For local-only testing, either configure jwt_validation.datasource_url for your registered datasource or temporarily set jwt_validation.enabled: false. Keep JWT validation enabled for a shared or public gateway.

Run the gateway

python main.py

Open http://localhost:8080 and confirm My GECX Agent appears in the dashboard.

3. Webex Contact Center BYOVA

  1. Register a BYOVA data source pointing at your gateway gRPC endpoint.
  2. Use schema 5397013b-7920-4ffc-807c-e8a3e0a18f43.
  3. In your WxCC flow, add the Virtual Agent / BYOVA element.
  4. Select agent name My GECX Agent (must match agents in config).

Deploying to Google Cloud Run

The repo includes a Dockerfile, .dockerignore, and a Cloud Run config (config/config.cloudrun.yaml) selected at runtime via the GATEWAY_CONFIG env var. main.py binds the gRPC server to the $PORT Cloud Run injects.

# 1. Enable APIs
gcloud services enable run.googleapis.com cloudbuild.googleapis.com \
  artifactregistry.googleapis.com --project YOUR_PROJECT_ID

# 2. Grant the Cloud Run runtime service account access to run CES sessions
#    (default compute SA unless you set --service-account on deploy)
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
  --member="serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
  --role="roles/ces.client"

# 3. Deploy from source (HTTP/2 is required for gRPC)
gcloud run deploy byova-gateway --source . --region us-central1 \
  --allow-unauthenticated --use-http2 --port 8080 --timeout 3600 \
  --project YOUR_PROJECT_ID

Notes:

echo '{"customer_org_id":"test"}' | grpcurl -import-path ./proto \
  -proto voicevirtualagent.proto -d @ SERVICE_HOST:443 \
  com.cisco.wcc.ccai.media.v1.VoiceVirtualAgent/ListVirtualAgents

How it works (implementation notes)

These are the non-obvious details that make WxCC <-> CES streaming work. They are worth understanding if you fork this connector.

Real-time streaming bridge

GECXStreamingSession runs a background thread per conversation that holds one CES BidiRunSession open. Before speech, it retains only bounded pre-roll. At START_OF_INPUT, it queues that pre-roll immediately; while speech is active, it forwards normalized caller audio as frames arrive while retaining only a small input_holdback_ms tail. END_OF_INPUT never requeues the complete utterance: it adds the unsent tail and codec-correct endpointing silence. CES server messages (recognition, agent text, TTS audio, interruption, and end-of-session) are mapped to BYOVA responses on an outbound queue.

The CES input queue is bounded by input_queue_max_chunks, and each audio item is at most input_stream_chunk_ms. A full queue applies the configured short put timeout and then terminates explicitly; it does not grow memory, silently drop audio, or accumulate unbounded latency. CES may recognize caller audio before gateway VAD commits END_OF_INPUT, but caller-owned output remains queued until that boundary so WxCC response ordering is preserved. The first CES response-audio frame can then reach WxCC before CES emits turn_completed.

CES can also produce an autonomous no-input prompt after the preceding caller-owned response has already reached FINAL. The connector publishes those chunks through the active gateway response sink immediately; it does not wait for a later caller frame to drain the connector queue. Autonomous chunks set is_barge_in_enabled from the connector’s barge_in_enabled setting. It defaults to false; set it to true only after the interruption path is validated end to end. When enabled, WxCC continues forwarding caller audio while CES waits for an interruption or a reply.

When gateway VAD emits START_OF_INPUT, the connector opens an isolated caller turn and waits for CES to acknowledge the streaming audio with a recognition result. An interruption signal does not open the gate because CES can complete the interrupted no-input turn immediately afterward. CES output produced before recognition belongs to that overlapping autonomous turn and is suppressed. The recognized post-input answer then uses the still-active WxCC response stream.

All terminal causes pass through one session-scoped decision guard. The first decision rejects later caller input, preserves already-queued audio chunks, half-closes the CES request stream once, and emits at most one terminal FINAL. Duplicate EndSession messages and late CES output are ignored.

Terminal cause WxCC outcome
CES EndSession with session_escalated=true (or a configured compatibility alias) TRANSFER_TO_AGENT
Normal CES EndSession SESSION_END
Turn-response timeout SESSION_END
CES GoAway SESSION_END
Unexpected CES stream failure/closure SESSION_END
WxCC cancellation or request-stream failure Silent CES cleanup; no continuation is expected
Normal WxCC request-stream half-close Preserve the CES session for the next RPC carrying the same conversation ID
Explicit gateway shutdown Silent CES cleanup

CES documents EndSession as ending the session and prohibiting further input. CES documents GoAway as requiring half-close and reconnection. This connector deliberately ends the WxCC virtual-agent session on GoAway; reconnection, retry, and backoff are not implemented here.

Speech boundaries

The gateway’s central Silero observer owns Webex START_OF_INPUT and END_OF_INPUT events. GECX does not run a second local speech detector: every caller-audio frame is ingested while CES responses are being produced. At an apparent speech end, the gateway holds END_OF_INPUT for speech_end_grace_ms (default: 1000, maximum: 2000). With the default end_silence_ms value, this creates a bounded two-second natural-pause window. If speech resumes in that window, GECX discards only its unsent tail, forwards up to input_pause_preroll_ms of resumed onset, and keeps one CES input turn without emitting a duplicate endpoint. Audio already streamed to CES is never retracted. Otherwise it commits the boundary by forwarding the held tail and configured endpointing silence. Configure the observer under the top-level voice_activity_detection block in config/config.yaml.

For lower completed-turn latency, set recognition_assisted_endpointing_enabled: true. Once the gateway has both a local VAD pause and a CES recognition result for the current caller turn, it uses recognition_assisted_grace_ms (default: 200) instead of the remaining fixed speech-end grace. Recognition received before the VAD pause is remembered for that active turn; recognition received during the full grace replaces that timer with the shorter, cancellable timer. If speech resumes, the timer is cancelled and new recognition is required before a later pause can use the short grace. If CES recognition never arrives, the normal speech_end_grace_ms path is unchanged. This changes semantic turn commitment, not transport: caller audio continues streaming frame by frame throughout.

Keep the option disabled until representative natural-pause calls and CES timeout cases have been tested. The key timing logs are gateway_input_acknowledged_before_vad_pause, gateway_recognition_assisted_endpointing, gateway_caller_speech_end_detected, gecx_caller_endpoint_committed, and gecx_first_response_audio.

Output audio: raw 8 kHz mu-law BYOVA chunks

CES normally streams TTS output as small frames. The connector places normal frames directly in Prompt.audio_content with response_type=CHUNK. Greeting and caller-triggered reply chunks keep is_barge_in_enabled=false. Autonomous no-input chunks use the barge_in_enabled connector setting, which defaults to false; when enabled, it prevents an open CES no-input prompt from blocking caller interruption. If the first CES frame is both longer than output_leading_audio_min_ms and contains no sustained speech, the connector drops low-energy frames until speech is detected and keeps output_speech_preroll_ms before that boundary. It does not accumulate a full turn and does not add a WAV header.

The normal response sequence is:

START_OF_INPUT (standalone FINAL event)
END_OF_INPUT   (CHUNK)
audio          (CHUNK)
audio          (CHUNK)
...
turn complete  (FINAL)

A terminal turn uses the same audio chunks, followed by one FINAL carrying TRANSFER_TO_AGENT or SESSION_END. The initial greeting uses the same CHUNK/FINAL pipeline.

The WxCC CHUNK path intentionally remains 8 kHz mu-law. The provider output can be LINEAR16 at 24000 Hz, which the connector anti-alias filters, downsamples, and mu-law encodes once, or MULAW at 8000 Hz for direct CES mu-law. The 24 kHz Linear16 path is recommended when CES direct mu-law sounds quiet or degraded. Unsupported provider-output pairs fail configuration early.

Gateway START_OF_INPUT discards already-buffered autonomous prompt output and isolates CES output until CES recognizes the caller audio. This prevents the interrupted no-input prompt from finalizing the response stream needed for the caller’s answer. Barge-in is limited to autonomous output; ordered greeting and caller-response chunks do not advertise interruption.

Input audio

WxCC sends 8 kHz mu-law. The gateway forwards WxCC’s declared encoding and sample rate in audio_metadata; the connector normalizes/converts to the CES InputAudioConfig format. force_input_format: "wxcc" remains a compatibility fallback for clients that omit that metadata.

Escalation to a human agent

There is no separate “transfer to live agent” streaming message in the CES API. When a CX Agent Studio agent escalates, CES sends an EndSession message — the same signal used for a normal “goodbye” — carrying a metadata Struct. The metadata is the only thing that distinguishes an escalation from a normal end.

Flow:

Agent escalates ─► CES EndSession { metadata: {...} }
                 ─► GECXConnector inspects metadata
                     ├─ looks like a transfer ─► TRANSFER_TO_AGENT ─► WxCC routes to a human queue
                     └─ otherwise             ─► SESSION_END (call ends)

1. Configure the agent to escalate (GECX side)

  1. In your agent’s instructions/playbook, define when to hand off (e.g. “if the caller asks for a human, or after two failed attempts, escalate”).
  2. Make that escalation end the session and attach session_escalated=true metadata. This is the canonical CES escalation signal and is checked first. For compatibility, the connector also recognizes any of these truthy metadata keys: transfer, transfer_to_agent, transfer_to_human, escalate, escalation, escalated, session_escalated, handoff, human_handoff, live_agent_handoff — and also a reason/type/status/intent/action value containing transfer, escalat, human, live agent, or handoff. Optionally include a reason string.

2. Discover exactly what your agent sends

The connector logs metadata key names on every session end without logging their values:

[<conv>] [GECX] EndSession metadata keys: ['reason', 'session_escalated']

Trigger one escalation, read that log line, and confirm your keys match. If they differ, either adjust the agent or override the detection in config (below) — no code change needed:

    # Match whatever your agent actually emits
    transfer_metadata_keys: ["handoff", "route_to_agent"]
    transfer_reason_keywords: ["transfer", "escalat", "human", "handoff"]
    transfer_reason_metadata_keys: ["reason", "type", "action"]

For short-lived debugging only, log_raw_terminal_metadata_debug: true exposes the full metadata at DEBUG level. Leave it disabled when metadata may contain customer data or sensitive identifiers.

When detected, you’ll see:

gecx_terminal_decision conversation_id=<conv> session=<session> reason=escalation outcome=transfer source=ces_end_session ...

3. Handle it in the WxCC flow

The Virtual Agent element emits a Transfer branch on TRANSFER_TO_AGENT. Wire that branch to a queue that routes to human agents. (A normal SESSION_END ends the virtual-agent interaction without a transfer.)

When CES includes its final spoken announcement with EndSession, the GECX connector streams the announcement as ordered CHUNK responses and follows it with one prompt-free terminal FINAL. The GECX path no longer calculates a WAV playback delay; the response stream itself carries the required order.

The connector retains CES text as transcript/fallback state but leaves it off audio chunks. This prevents WxCC from synthesizing a duplicate text prompt. For GECX, START_OF_INPUT remains an immediate standalone FINAL event so WxCC continues forwarding caller audio through a bounded natural pause. END_OF_INPUT begins the output stream as a CHUNK event; audio chunks follow it, and the output turn closes with one FINAL.

Caller audio is buffered from a bounded pre-roll through the gateway’s Silero speech-end boundary, then sent to CES as one contiguous turn. This prevents a natural pause inside an utterance from becoming an unintended CES barge-in. After a terminal-sounding response, the connector also allows a short grace window for an EndSession that follows the final TTS frames.

Configuration reference

Key Required Description
project_id Yes GCP project ID
location Yes Region (e.g. us)
application_id Yes CX app ID
deployment_id No Published deployment ID; omit to use the app root/draft agent
deployment No Full deployment resource path (alternative to deployment_id)
entry_agent No Full agent resource path to run a specific sub-agent
api_endpoint No CES endpoint; defaults to ces.<location>.rep.googleapis.com
service_account_key No Path to SA JSON; omit to use ADC
oauth_client_id No Client ID for the interactive installed-app OAuth flow
oauth_client_secret No Client secret for the interactive installed-app OAuth flow
oauth_token_file No Authorized-user JSON cache (default: gecx_oauth_token.json)
initial_message No Text sent when the CES stream opens (default: Hello)
enable_partial_responses No Request CES text streaming for logs, terminal-cue detection, and text-only fallback
barge_in_enabled No Allow interruption of autonomous/no-input prompt playback (default: false; greeting and caller-triggered replies always remain non-bargeable)
output_sample_rate_hertz No CES output rate: 24000 with LINEAR16 (recommended) or 8000 with MULAW (default)
output_audio_encoding No CES output codec: LINEAR16 for connector-side conversion or MULAW for direct CES output
suppress_long_leading_audio No Guard anomalously long low-energy CES output before prompt speech (default: true)
output_leading_audio_min_ms No Minimum first-frame duration that can activate the guard (default: 5000)
output_speech_rms_threshold No 16-bit linear RMS threshold used to identify speech in decoded mu-law frames (default: 200)
output_speech_start_frames No Consecutive 20ms frames required to open the output gate (default: 2)
output_speech_preroll_ms No Audio retained immediately before detected speech (default: 100)
force_input_format No wxcc forces 8 kHz MULAW when input metadata is unavailable
turn_response_timeout_seconds No Maximum wait after gateway speech end for CES to complete the agent turn (default: 30)
endpointing_silence_ms No Codec-correct silence appended after the progressively streamed caller tail for CES endpoint detection (default: 2000; one second may leave a turn open until more audio arrives)
input_preroll_ms No Bounded caller audio retained before gateway speech start to avoid clipping (default: 500)
input_holdback_ms No Small unsent active-audio tail used for pause/resume and committed endpointing (default: 250, maximum: 500)
input_pause_preroll_ms No Maximum onset audio retained while a possible speech end is held, then merged if the caller resumes (default: 250)
input_stream_chunk_ms No Maximum duration of each queued CES caller-audio item (default: 100, range: 20-100)
input_queue_max_chunks No Maximum pending CES input items before explicit backpressure failure (default: 20, range: 1-200)
input_queue_put_timeout_ms No Maximum wait for CES input queue capacity before terminating the session (default: 50, maximum: 1000)
terminal_response_grace_seconds No Wait for delayed EndSession after a terminal-sounding TTS turn (default: 3)
transfer_metadata_keys No EndSession metadata keys that, when truthy, trigger a human transfer (see Escalation)
transfer_reason_keywords No Substrings that, if found in a reason/type metadata value, trigger a transfer
transfer_reason_metadata_keys No Which metadata keys are scanned for transfer_reason_keywords
log_raw_terminal_metadata_debug No Log raw EndSession metadata at DEBUG level; defaults to false because values may be sensitive

Authentication options

Method Config keys Notes
Service account service_account_key Recommended for production
ADC (none) gcloud auth application-default login for dev
OAuth oauth_client_id, oauth_client_secret, optional oauth_token_file Interactive browser flow; token JSON is saved atomically with owner-only permissions
Access token access_token Short-lived (~1 hour), not for production

Outside Google Cloud, ADC can also use a credential configuration referenced by GOOGLE_APPLICATION_CREDENTIALS. Keep credential files outside the repository and grant the represented identity roles/ces.client. The connector does not deserialize pickle credentials. If an older checkout created gecx_oauth_token.pickle, remove it and complete the OAuth flow once to create the JSON cache.

Troubleshooting

Symptom Check
Agent not listed in WxCC agents name in config matches flow selection
Stream fails on start roles/ces.client, API enabled, correct location
404 / UNIMPLEMENTED on BidiRunSession Wrong endpoint — must be regional ces.<location>.rep.googleapis.com (auto-derived from location)
429 Resource exhausted CES per-app session quota; retry/backoff or request more quota
No audio to caller (silence) Confirm gecx_first_response_audio appears, the next response is a BYOVA CHUNK, and output remains MULAW / 8000. See Output audio.
Long static/noise before a prompt Look for gecx_long_leading_audio_detected followed by gecx_leading_audio_suppressed; tune the guarded output settings only with captured CES evidence.
Garbled speech Confirm the gateway logs the declared WxCC encoding/sample rate; use force_input_format: "wxcc" only when the client omits metadata
Agent recognizes the caller but its reply is not audible Confirm gecx_pre_input_output_suppressed is followed by gecx_caller_input_acknowledged, gecx_first_response_audio, and gecx_streamed_turn_complete for the same conversation.
CES logs a no-input prompt but the caller does not hear it Confirm gecx_first_response_audio reports delivery_mode=async; its barge_in_enabled value should match connector configuration. Verify the active WxCC stream did not cancel before that timestamp.
Session ends with input backpressure Correlate gecx_input_queue_backpressure with CES/network health. Increase input_queue_max_chunks only after measuring frame cadence and acceptable latency; do not mask a stalled CES stream with an unbounded queue.
No response after END_OF_INPUT Check for turn_completed or a turn-completion timeout in [GECX] logs; increase turn_response_timeout_seconds if the agent regularly needs more than 30 seconds
GoAway from CES The connector intentionally emits one SESSION_END and half-closes CES; it does not reconnect in the current implementation
Import error pip install google-cloud-ces

Logs

Search gateway logs for [GECX]: