nemo_voice_agent.evaluation.bridge
nemo_voice_agent.evaluation.bridge
Voice Agent Evaluation Bridge
Connects two voice agents via WebSocket and provides:
- Bidirectional audio routing
- Response latency measurement
- Dynamic system prompt updates via RTVI actions
- Conversation monitoring and metrics
Module Contents
Classes
Data
API
Metrics collected during evaluation
Calculate latency statistics
Reset all metrics to prepare for a new scenario
Single response latency measurement
Entry for segLST format (segment list with timing)
Evaluation bridge that connects two voice agents via WebSocket and provides control through RTVI actions.
Key features:
- Routes audio bidirectionally between agents
- Monitors transcriptions and metrics
- Measures response latency by tracking audio frames
- Can send RTVI control messages to update prompts
- Works with distributed agents
Send apply_initialization to BOTH bots — always, even with no init actions.
Bot-side dispatch via the apply_initialization RTVI action.
The handler does three things per bot:
- Merge
shared_state_initJSON into the bot’sshared_state(preserving runtime sentinels stashed byupdate_system_prompt). - If
db_pathis in the merged state, resolve it againstEVAL_DATA_ROOTand replace with the loadeddbdict. - Dispatch each per-side init function record (filtered by
side) against the now-loadeddb.
Always called once per bot, even when scenario.initialization_actions
is empty — steps 1 and 2 (state merge + DB load) must run for every
tau2 scenario regardless. Single-side domains (eva / airline / retail)
whose setup_shared_state populates db_path rely on this call
to trigger the DB load. The bot’s handler is fast (~tens of ms) when
actions list is empty.
Failure handling: any per-bot success: false response
(invalid JSON, missing DB file, missing init function in the bot’s
registry, dispatcher exception) raises RuntimeError so the
calling prepare_for_scenario aborts before the conversation
starts. Partial seeding produces noisy / unscoreable runs.
Build conversation log entries from finalized segments with computed latencies.
Called after all segments are finalized so that latency calculation has access to all user and agent segments. For each agent segment, latency is computed as: agent.start_time - previous_user.end_time Positive = normal response delay, negative = agent interrupted/barged in early.
Applies turn_start_offset_secs and turn_end_offset_secs to match seglst timestamps.
Finalize the current in-progress turn for the given speaker.
Sets end_time, assigns transcript (or “[INTERRUPTED]” if no TTS text was received), appends the segment to self.metrics.segments, and clears accumulation state.
Parameters:
“user” or “agent”
Absolute timestamp (asyncio loop time)
Returns: Optional[SegmentEntry]
The finalized SegmentEntry, or None if no segment was in progress.
Format a turn entry for the conversation log.
Parameters:
“user” or “agent”
Transcript text
Turn start time (relative to scenario start)
Turn end time (relative to scenario start)
Optional response latency in milliseconds
Returns: str
Formatted log entry string
Get time relative to scenario start (thread start time).
Parameters:
Absolute timestamp (asyncio loop time)
Returns: float
Time in seconds relative to thread_start_timestamp, or 0 if not set
Emit a uniform side-tagged DEBUG log line for an RTVI event.
Each monitor (_monitor_user_message / _monitor_agent_message) is
bound to one side by construction, so we know which side emitted the
event without time-correlation. This helper writes a one-line tag plus
expanded payload lines for the message types that carry analytically
useful structured data (TTFB values, token counts, transcription text,
RTVI action names).
Skipped types (_RTVI_TYPES_ALREADY_TAGGED): the monitors already
emit dedicated side-tagged lines like [AGENT TTS] <text> for these,
so adding [AGENT EVENT] type=bot-tts-text on top would just
duplicate. Their expanded payload (the TTS text) is already in the
existing line.
Skipping is by event-tag emission only — the underlying RTVI message
still flows through pipecat’s ProtobufFrameSerializer:deserialize
DEBUG log, so nothing is lost from the bridge log.
Monitor agent messages for timing and transcripts.
Turn lifecycle: BOT_STARTED_SPEAKING → BOT_TTS_TEXT (accumulate) → BOT_STOPPED_SPEAKING (finalize). Latency is measured from user’s last audio to agent’s first audio frame.
Monitor user messages for timing and transcripts.
Turn lifecycle: BOT_STARTED_SPEAKING → BOT_TTS_TEXT (accumulate) → BOT_STOPPED_SPEAKING (finalize).
Replay one action onto the shadow DBs, run scenario.sync_state, dispatch deltas.
Called from _monitor_agent_message / _monitor_user_message
when an action-applied RTVI server message arrives. source_side
is which side’s monitor saw it.
No-op when sync_enabled is False (single-side scenarios).
Receive audio from agent WebSocket and put into queue for user thread.
Receive audio from websocket and put into queue.
Parameters:
Source websocket to receive from
How long to run the receive loop in seconds
For logging (e.g., “USER→AGENT”, “AGENT→USER”)
Thread-safe queue to put audio chunks into
Async monitoring function for metrics (e.g., _monitor_user_message)
Receive audio from user WebSocket and put into queue for agent thread.
Resample audio array using soxr. Returns int16 array.
Retrieve the context history from the WebSocket. First send a message to the ws to trigger the
get_context_history RTVI action, then wait for the response.
Args:
ws: WebSocket connection
Returns:
context_history: context history as a dictionary with two keys: context and logs,
where context the LLM context history, and logs is the bot server logs.
Retrieve the scenario summary from the bot via the
get_scenario_summary RTVI action. Mirrors _retrieve_context_history.
Parameters:
WebSocket connection to the agent bot.
When True, ask the bot to inline the db
dict alongside the hash. Set by the runner via
Scenario.db_state_assertions (predicate evaluation needs
the actual DB, not just a hash). Off by default to preserve
the existing hash-out behavior for retail (whose 7 MB DB
would exceed pipecat’s 1 MB WS frame cap).
Returns: dict
Dict with at minimum {"actions": list, "db_hash": str|None}.
Save final sent audio chunks to disk as stereo WAV for debugging.
Save the bot server context history to a JSON file under the output directory.
Build and write conversation log entries sorted by start time, with computed latencies.
Save the agent’s final response to a JSON file under the output directory.
Two sources, in priority order:
- Pull (
self.scenario_summary["actions"]) — the bridge-pulled auto-aggregated action list. Used when the bot registered theget_scenario_summaryaction (post-commit-3 bots) and returned a non-empty actions list. - Push (
self.metrics.agent_final_response) —<final_response>text messages captured during the conversation. Used by domains that still have an LLM-callable summary tool (restaurant / customer_service / qa) or as a fallback when pull returned empty.
Output is always list-wrapped ([{"actions": ...}]) for shape compat
with the existing strict comparator and downstream consumers.
Save the post-run scenario DB hash(es) to final_scenario_db_hash.txt.
Sourced from the bridge-pulled scenario_summary["db_hash"] (and
["user_db_hash"] for telecom). Skipped if no pull happened (legacy
bots) or the hash is None. Used by the runner’s DB-state matching
when scenario.expected_scenario_db is set — the runner compares
the expected-DB hash (computed in-process from its own gold replay)
against the bot’s reported hash.
Hash-only design: the full DB stays on the bot server. See
create_get_scenario_summary_action for the rationale (WebSocket
frame size limit; tau2’s DB is 7 MB while pipecat’s default frame
cap is 1 MB).
Save segLST transcript file with offset-adjusted timestamps.
Save the user and agent context history to a JSON file under the output directory.
Get audio from queue, process through AudioStream, send to user WebSocket.
Send one apply_initialization action and wait for the result.
side_label is the bot label ("agent" or "user") used only
in log lines and error messages — the bot itself doesn’t need to know
which side it is, since each action in the payload already carries
its own side field that the dispatcher routes by.
Raises:
RuntimeError: if the bot returnssuccess: falseor the request times out. The caller (_apply_initialization) propagates this up soprepare_for_scenarioaborts cleanly.
Send a single apply_sync_delta RTVI action to a bot.
Mirrors _send_apply_initialization: builds an action
message with a unique id, awaits the response, logs success or
failure. Failures are warnings — sync drift is recoverable as
long as the next propagation cycle eventually catches up.
Send audio stream at fixed intervals from AudioStream with duration and grace period.
Parameters:
AudioStream containing buffered and resampled audio
Destination websocket to send to
For logging (e.g., “USER→AGENT”, “AGENT→USER”)
How long to run the send loop in seconds
Queue to retrieve audio chunks from
List to append sent chunks to for tracking
Send RTVI client-ready handshake and wait for bot-ready
Send RTVI reset action to clear conversation history.
Parameters:
WebSocket connection
Name of agent (for logging)
Get audio from queue, process through AudioStream, send to agent WebSocket.
Prepare shadow DBs + tool map for cross-side state propagation.
Called from prepare_for_scenario once per scenario,
after _apply_initialization so the bot-side
live state is already at its post-init starting point. We
mirror that here by replaying the same init actions onto the
shadow DBs, then run a one-shot sync_state to propagate
any cross-side state that should be coherent at conversation
start (e.g. agent-side set_data_usage(15.1) flipping
user-side surroundings.mobile_data_usage_exceeded to True).
No-op when the scenario’s sync_state is the inherited
default (single-side domains).
Thread 2: Handle all agent WebSocket traffic (bidirectional).
This thread:
- Gets user audio from user_to_agent_queue
- Sends user audio to agent WebSocket
- Receives audio from agent WebSocket
- Puts agent audio into agent_to_user_queue for user thread
Parameters:
How long to run (seconds)
Connect to both user and agent with retry logic
Parameters:
Maximum number of connection attempts per endpoint
Initial delay between retries (doubles each retry)
Disconnect from both user and agent.
Parameters:
If True, print final latency statistics (default: True) Set to False when disconnecting during scenario resets
Get evaluation metrics
Initialize the output directory and all derived log/audio file paths.
Prepare the bridge for a scenario.
Parameters:
A Scenario instance. The bridge calls methods
on it directly (get_user_prompt, get_agent_prompt,
setup_shared_state, initialization_actions,
sync_state, etc.) — no intermediate dict
serialization step. Single source of truth.
Per-scenario output directory.
Pipecat log level for the bot servers.
Reset metrics and both agents’ conversation history
Reset agent’s conversation history. Useful to clear context between evaluation scenarios.
Reset user’s conversation history. Useful to clear context between evaluation scenarios.
Route audio between agents and monitor conversation. Uses separate threads per WebSocket to eliminate asyncio contention.
Parameters:
Duration of the evaluation in seconds
Send a text message to the agent agent to trigger conversation.
Parameters:
Text to send to agent agent’s LLM
Send a text message to the user agent to trigger conversation.
Parameters:
Text to send to user agent’s LLM
Set the noise configuration
Update agent’s system prompt via RTVI action.
Scenario fixture data (db_path, custom keys from
Scenario.setup_shared_state) is NOT sent here — it flows via
the subsequent apply_initialization call instead.
Parameters:
New system prompt text
New tools in json string format
If True, also sends reset action after updating prompt
If True, add previously configured system prompt suffix to the new prompt
Registry namespace the bot server should use to look up
tools by name (e.g., "tau2_airline"). Falls back to
"default" per-tool if the name isn’t in the specified
domain (with a warning logged bot-side).
Update user’s system prompt via RTVI action.
Scenario fixture data (db_path, custom keys from
Scenario.setup_shared_state) is NOT sent here — it flows via
the subsequent apply_initialization call instead.
Parameters:
New system prompt text
New tools in json string format
If True, also sends reset action after updating prompt
If True, add previously configured system prompt suffix to the new prompt
Registry namespace the bot server should use to look up
tools by name. Stashed on bot-side shared_state["__tool_domain__"]
for write tools to read when emitting action-applied events.
Thread 1: Handle all user WebSocket traffic (bidirectional).
This thread:
- Receives audio from user WebSocket
- Puts user audio into user_to_agent_queue for agent thread
- Gets agent audio from agent_to_user_queue
- Sends agent audio to user WebSocket
Parameters:
How long to run (seconds)