The Builder API

View as Markdown

nemo_voice_agent/pipecat/services/nemo/builders.py holds one build_* function per pipeline stage. Each is a thin wrapper around a service constructor that reads the relevant block of the loaded ConfigManager, so a bot script does not repeat the same config plumbing. The builders are independent — import only the ones you need, and construct anything they do not cover inline.

Two bots in this repo use them: examples/generic_voice_agent/server/server.py and evaluation/bot_server.py. Both call the same eleven builders in the same order.

Every Builder

Each builder owns one runtime component and reads the corresponding configuration block.

BuilderReadsReturns
build_audio_logger(config_manager)transport.record_audio_data, transport.audio_log_dirAudioLogger, or None when recording is off. Session id is a timestamp.
build_vad_analyzer(config_manager)vad.* (using ConfigManager.get_vad_params()), transport.audio_in_sample_rateSileroVADAnalyzer. Never None.
build_vad_processor(vad_analyzer)nothing — takes the analyzer, not the configVADProcessor wrapping the analyzer, or None if you passed None.
build_ws_transport(config_manager, vad_analyzer, host, port)transport.audio_in_sample_rate, transport.audio_out_sample_rate, transport.audio_out_10ms_chunksSingleClientWebsocketServerTransport with a Protobuf serializer and session_timeout=None.
build_stt(config_manager, audio_logger=None)the whole stt blockAn STTService from get_stt_service_from_config. Never None.
build_diar(config_manager, audio_logger=None)diar.enabled, diar.model, diar.threshold, diar.frame_len_in_secs, and stt.deviceNemoDiarService, or None when diar.enabled is false.
build_turn_taking(config_manager, audio_logger=None, *, use_diar=None, use_vad=True)turn_taking.enabled, turn_taking.max_buffer_size, turn_taking.bot_stop_delay, turn_taking.backchannel_phrases_path, diar.enabledNeMoTurnTakingService, or None when turn_taking.enabled is false.
build_tts(config_manager, audio_logger=None)the whole tts blockA TTSService from get_tts_service_from_config. Never None.
build_llm_text_processor(config_manager)tts.use_text_aggregator, plus tts.extra_separator, tts.ignore_strings, tts.min_sentence_length, tts.use_legacy_eos_detectionLLMTextProcessor holding the segmenting text aggregator, or None when tts.use_text_aggregator is false.
build_llm(config_manager)the whole llm blockAn LLMService from get_llm_service_from_config. Never None.
build_context_and_aggregators(llm, config_manager, turn_taking=None)llm.system_role, llm.system_prompt (plus suffix), llm.inject_dummy_user_message, llm.dummy_user_message, turn_taking.enabledA 4-tuple: (context, user_aggregator, assistant_aggregator, original_messages).

Two helpers in the same module are not pipeline stages:

HelperReadsReturns
resolve_log_file_path(config_manager, default_name="bot_server.log")server.log_file, server.log_level, server.create_new_log(log_file, log_level, create_new_log), to pair with setup_rotating_log from nemo_voice_agent.utils.misc.
overwrite_existing_log(config_manager)server.overwrite_existing_logTrue to delete a pre-existing log on startup, False to rename it.

Details worth knowing:

  • build_ws_transport accepts vad_analyzer but ignores it. Since Pipecat 1.0 the input transport no longer runs VAD. The argument is kept only so existing call sites still work. Pass the analyzer to build_vad_processor and place the result right after transport.input() instead.
  • build_diar takes the diarization device from stt.device, not from diar.device.
  • build_diar and build_turn_taking accept audio_logger for call-site symmetry. Only build_turn_taking forwards it to the service. build_stt and build_tts forward it into their get_stt_service_from_config / get_tts_service_from_config factory.
  • build_turn_taking is annotated as returning NeMoTurnTakingService but returns None when turn_taking.enabled is false — treat it as optional like the others.
  • build_llm_text_processor exists because Pipecat 1.0 dropped TTSService(text_aggregator=...). Pipecat silently ignores unknown constructor kwargs, so passing an aggregator to the TTS service would fall back to plain sentence splitting with no error.

Optional Stages

build_audio_logger, build_diar, build_turn_taking, and build_llm_text_processor return None to mean “omit this stage”. build_vad_processor returns None only if you hand it None, and build_vad_analyzer always returns an analyzer — so VAD is never omitted on the shipped path. Assemble the pipeline behind if x is not None checks rather than leaving placeholders:

1pipeline_list = [ws_transport.input()]
2if vad_processor is not None:
3 pipeline_list.append(vad_processor)
4pipeline_list.extend([rtvi, stt])
5if diar is not None:
6 pipeline_list.append(diar)
7if turn_taking is not None:
8 pipeline_list.append(turn_taking)
9pipeline_list.extend([user_agg, llm])
10if llm_text_processor is not None:
11 pipeline_list.append(llm_text_processor)
12pipeline_list.extend([tts, ws_transport.output(), assistant_agg])

Call Order

Three dependencies constrain the order:

  1. build_vad_analyzer before build_vad_processor and build_ws_transport.
  2. build_llm before build_context_and_aggregators.
  3. build_turn_taking before build_context_and_aggregatorsand pass its result through.

The third dependency matters most. In Pipecat 1.0+, the pipeline permits exactly one component to emit UserStartedSpeakingFrame and UserStoppedSpeakingFrame. The turn_taking argument determines that component. Given a service, the builder selects ExternalUserTurnStrategies so NeMoTurnTakingService owns turn detection and the aggregator stays quiet. Passing None is indistinguishable from omitting the argument — None is the parameter’s default — so in both cases the builder re-derives the answer from turn_taking.enabled. When that key is false (the *_nvidia.yaml configs) it builds UserTurnStrategies from VADUserTurnStartStrategy plus SpeechTimeoutUserTurnStopStrategy, so the aggregator drives the turn directly from VAD frames. When the key is true or absent (the shipped default.yaml), it still selects ExternalUserTurnStrategies. Nothing is then left in the pipeline to emit the user-turn frames. That fallback is correct for the stock builders but not for a bot that constructs its turn-taking service inline. Such a bot must pass the service explicitly.

original_messages, the fourth tuple element, is a fresh deep copy of the initial message list. Hand it to the reset and update-prompt RTVI handler factories. Refer to RTVI Actions.

Substituting Your Own Service

Every builder returns a stock Pipecat base type, so swapping one out is a one-line change in the bot script. Replace the call, keep the object in the same position in pipeline_list, and the rest of the pipeline is unaffected.

1# Instead of: stt = build_stt(config_manager, audio_logger)
2from my_package.stt import MySTTService # must subclass pipecat STTService
3
4stt = MySTTService(model="my-model", sample_rate=16000, audio_passthrough=True)

The contracts your replacement must honor:

SlotBase Class to Subclass
STTpipecat.services.stt_service.STTService (NemoDiarService also subclasses this)
TTSpipecat.services.tts_service.TTSService
LLMpipecat.services.llm_service.LLMService; build_context_and_aggregators is typed against BaseOpenAILLMService
Turn taking, VAD, audio bufferingpipecat.processors.frame_processor.FrameProcessor

Two extra behaviors are duck-typed rather than enforced by the base classes:

  • Reset. The bot passes a resettable list to create_reset_context_action. The handler calls .reset() on each entry that defines one and skips None entries, so a service without reset() is not reset between scenarios.
  • Tool calling. A service only exposes tools if it mixes in ToolCallingMixin from nemo_voice_agent/utils/tool_calling/mixins.py and is listed in the tool_mixins argument of register_direct_tools_to_llm. Refer to Custom Tools.

If your service needs its own config, add a block to the YAML and read it from config_manager.server_config. ConfigManager passes unknown keys through untouched, so you do not need to thread new arguments through the shipped builders. Refer to Server Config.

Next Steps

Choose the next guide based on whether you need to replace an assembly or transform frames within it: