Extension Overview
NeMo Labs Voice Agent is a Pipecat pipeline assembled from a small set of builder functions. You can change the models that builders instantiate, the processors in the chain, or the assembly code. Choose the lowest tier that supports your requirement.
Workflow Overview
Choose one of three extension tiers, starting with the least invasive option that supports your change.
Key Concepts
Both shipped entrypoints use the same pipeline shape, builders, and extension boundaries.
Two Entrypoints, One Pipeline
Everything on this page applies to both shipped entrypoints. They differ only in their config directory and the number of real-time voice inference (RTVI) control-plane handlers that they register.
Both define run_bot_websocket(), both import the same build_* functions from nemo_voice_agent/pipecat/services/nemo/builders.py, and both assemble the same shape:
Bracketed stages are dropped when their builder returns None. These stages include Diar when
diar.enabled is false and TurnTaking when turn taking is disabled. They also include LLMTextProcessor
when tts.use_text_aggregator is false and UserAudioBuffer for non-omni models
(llm.is_omni_model). Refer to
How It Works for the runtime view.
Three Tiers
The following tiers describe what each extension approach changes and where to implement it.
Tier 1 — Swap Models via YAML
ConfigManager (nemo_voice_agent/utils/config_manager.py) reads the file named by the SERVER_CONFIG_PATH
environment variable. It gives each section to the matching builder: stt: to build_stt, diar: to
build_diar, llm: to build_llm, and tts: to build_tts. It also maps vad: to build_vad_analyzer,
turn_taking: to build_turn_taking, and transport: to build_ws_transport.
The two config directories use different structures:
- Demo configs (
default.yaml,default_nvidia.yaml) delegate per-model parameters to a sub-YAML throughmodel_config:. The field points intollm_configs/,tts_configs/, orstt_configs/. The sub-YAML overrides the top-level file, so its value wins when both files set the same key. Editing that key indefault.yamlalone has no effect. - Eval configs (
agent.yaml,agent_nvidia.yaml,agent_nvidia_omni.yaml,user.yaml,user_nvidia.yaml) are flat and self-contained. They have nomodel_config:key or sub-config directories underevaluation/server_configs/. Sub-config paths resolve against the entrypoint’s directory, so a relative path cannot point an eval config at a demo sub-YAML. Edit the eval file directly.
Reasoning mode adds another condition. The automatic swap to a sibling *_think.yaml fires only when
server.use_model_registry: true, the model appears in model_registry.yaml, and that entry sets
reasoning_supported: true. An explicit llm.model_config: short-circuits the registry lookup, so the
shipped default.yaml never swaps. Point model_config: at the _think.yaml yourself. The eval configs set
use_model_registry: false and pass the toggle directly to vLLM by interpolating ${llm.enable_reasoning}
into vllm_generation_params. For details, refer to
Reasoning Mode and
Model Registry.
What Tier 1 covers without any Python:
Run a demo server against your edited config:
Run an eval bot against yours. SERVER_CONFIG_PATH resolves against the current working directory, so the cd is required:
Full key reference: Server Config and Config Schema.
Tier 2 — Insert a Frame Processor
When no builder exposes the required transformation between two stages, write a FrameProcessor subclass.
Then insert it into pipeline_list before the Pipeline(...) call. Both entrypoints build that list inline,
so the change is localized.
Three rules cover most processors: call super().process_frame(frame, direction) first, filter with
isinstance so unrelated frame types pass through untouched, and always finish by pushing the frame onward.
For worked examples, refer to Custom Frame Processors.
Tier 3 — Replace the Assembly
At this tier you write your own run_bot_websocket() and choose every service yourself. Two things stay fixed.
A Pipecat WebSocket transport. build_ws_transport returns a SingleClientWebsocketServerTransport with a
ProtobufFrameSerializer. That pairing is the wire contract that browser clients and the evaluation bridge
both use. For protocol details, refer to Client Protocol.
An RTVIProcessor with handlers registered. Handler factories live in
nemo_voice_agent/pipecat/processors/frameworks/rtvi_actions.py. Each returns a
(message_type, handler) pair that you pass to register_client_message_handlers. A demo server needs only
reset. An eval bot needs all six because the bridge drives scenario lifecycle over them.
An unregistered type produces an error-response rather than a hang, so a missing handler shows up
immediately in the bridge log. For skeleton code, refer to
Building Your Own Pipeline. For message payloads, refer to
RTVI Control Plane and RTVI Messages.
Verify Your Customization
Whichever tier you used, exercise it against one fast scenario before a full run. Three terminals, all started from evaluation/:
Then read the per-scenario output directory:
Related Pages
Use these pages for implementation details and evaluation integration:
- The Builder API — every
build_*function and the config keys it reads. - Writing Your Own Tools — extending the agent’s capabilities rather than its pipeline.
- Evaluating External Agents — pointing the harness at a bot you built yourself.