Writing Your Own Tools
NeMo Labs Voice Agent gives you three ways to expose a Python callable to the large language model (LLM). Read Tool Calling first for backend requirements and the config flags that gate registration.
The first two paths both hand Pipecat a DirectFunction, so they share one contract.
Prerequisites
Before you add a tool, complete the following preparation:
- Configure a supported backend and parser by following Tool Calling.
- Decide whether the callable is standalone, owned by a component, or needs an explicit schema.
- Identify the pipeline entrypoint where you want to register the tool.
The Direct-Function Contract
pipecat/adapters/schemas/direct_function.py validates and introspects every direct function at
registration time. Violating the first two rules raises at startup, not at call time.
- The function must be
async. - Its first parameter must be named exactly
params(it receives aFunctionCallParams). - It must deliver its result exactly once, by awaiting
params.result_callback(...).
Everything the LLM sees is derived automatically:
Because the docstring is the prompt, write it for the model. State when the tool should be called, when it should not be called, and what the agent should do with the result.
1. Direct Functions
The shipped example is tool_get_city_weather in nemo_voice_agent/utils/tool_calling/basic_tools.py. It
pushes a TTSSpeakFrame filler (“Looking up weather data for … Please wait a moment.”) before its network
call, so the user is not left in silence. The tool wraps the HTTP call in asyncio.wait_for and returns
{"error": ...} through result_callback on timeout or failure.
A minimal tool of your own:
Register it with the pipeline by adding it to the existing register_direct_tools_to_llm call in
examples/generic_voice_agent/server/server.py (guarded by llm.enable_tool_calling):
The full signature (keyword-only, from nemo_voice_agent/utils/tool_calling/mixins.py):
Tools already present on the context are preserved — the helper appends to context.tools before calling
context.set_tools(...), so calling it twice accumulates rather than replaces.
2. Component-Owned Tools
When a tool has to reach into a live pipeline component, put it on the component. Mix
ToolCallingMixin (nemo_voice_agent/utils/tool_calling/mixins.py) into the service and implement
setup_tool_calling, which registers each bound method:
The mixin surface is three members:
Name matching: the function_name you pass to register_direct_function is only a bookkeeping key.
register_direct_tools_to_llm appends the callable, and Pipecat names the tool after the Python method,
so a mismatched key silently has no effect on what the model sees. Keep them identical.
The canonical example is KokoroTTSService in nemo_voice_agent/pipecat/services/nemo/tts.py. It registers
six voice-control methods: tool_tts_set_speed, tool_tts_reset_speed, tool_tts_speak_faster,
tool_tts_speak_slower, tool_tts_set_voice, and tool_tts_reset_voice. BaseNemoTTSService calls
setup_tool_calling() from its constructor, so a subclass only overrides the method. Two patterns are worth
copying. tool_tts_set_voice pushes an LLMTextFrame("Just a moment.") before reloading the model. It runs
the blocking reload through asyncio.to_thread so the event loop continues serving audio.
Pass the instance in tool_mixins. Anything in that list that is not a ToolCallingMixin is skipped with a
warning. The example server can therefore pass tool_mixins=[tts] unconditionally even when tts.type
resolves to the hosted NVIDIA service.
3. Schema Tools
StandardSchemaTool in nemo_voice_agent/utils/tool_calling/base.py is the explicit-schema path. Subclass
it and implement three members. The base builds the FunctionSchema and owns delivery:
Key differences from the direct path:
- The tool name comes from an optional class-level
nameattribute, falling back to the class name — so snake_case tool names are possible here. _executeis pure: it takes the call arguments as keyword arguments and returns a result. It must not touchparamsand must not callresult_callback.__call__delivers exactly once, and converts a raised exception into{"error": ...}.tests/unit/test_tool_call_contract.pypins this.- Falsy results are wrapped before delivery, because Pipecat rewrites any falsy result to the literal string
"COMPLETED"— which a model reads as success. A bare[]from a lookup that matched nothing becomes an explicit “No matching records found.” envelope instead. register_schema_tools_to_llmalso installs a catch-all handler for unregistered tool names that returns the list of tools available to the model.
Register with register_schema_tools_to_llm(llm, context, tools, cancel_on_interruption=True, keep_existing_tools=True, register_unknown_tool_handler=True). This is the path the evaluation domains use —
refer to Authoring Evaluation Tools.
Interruption and Long-Running Tools
cancel_on_interruption defaults to True on both registration helpers, and that is what you want for an
ordinary tool. Setting it to False does more than survive a barge-in: in Pipecat 1.x it also marks the tool
asynchronous, so the LLM does not wait for the result. The model immediately receives a
{"status": "running"} placeholder and the real payload is injected later as a developer message. Only opt
in for a genuinely fire-and-forget tool. For bounded waits, use llm.function_call_timeout_secs
(default 10.0). Refer to Tool Calling.
Testing Your Tool
Both paths are testable without a GPU or a running LLM: build a duck-typed stand-in for
FunctionCallParams that records result_callback calls, then await the tool.
tests/unit/test_runtime_basic_weather_tool.py does this for the weather tool.
tests/unit/test_tool_call_contract.py does it for the schema-tool contract.
Two repository-wide requirements apply before committing. Every .py file other than __init__.py needs the
SPDX/Apache header in its first 10 lines. CI fails without it. Ruff is the only formatter and linter. Run
uv run ruff format my_tools.py and uv run ruff check --fix my_tools.py.
Next Steps
Continue with the runtime or reference guide that matches the tool surface you are implementing:
- Tool Calling — backend support, parser flags, and the shipped demo tools.
- Authoring Evaluation Tools — the schema-tool path in depth.
- Prompts — tuning
system_prompt_suffixso the model reaches for tools at the right time. - Server Configuration — where
enable_tool_callinglives and how layering works.