Add client-side tool calling to push-to-talk example - #241
Open
jamsea wants to merge 7 commits into
Open
Conversation
Show how to run an LLM tool call in the browser instead of on the server, using the current RTVI flow rather than the deprecated RTVIProcessor.handle_function_call. The server advertises a get_browser_info tool and registers a handler that returns without calling result_callback, which leaves the call open. The RTVI observer tells the client the call is in progress, the browser runs it, and the client's llm-function-call-result completes the call so the LLM can talk about the result. Client-run tools must opt in to a FULL function_call_report_level. The default is NONE, which sends only a tool_call_id, and a client cannot dispatch a call it cannot name. push-to-talk already demos client-to-server messaging via sendClientMessage, so this completes the two-way picture in one example. It also has a single client, which matters here: a tool the client never answers leaves the call open for the rest of the session, since function_call_timeout_secs does not cover deferred calls. Also promotes @pipecat-ai/client-js and client-react from transitive to direct dependencies, since the example now relies on registerFunctionCallHandler firing off llm-function-call-in-progress (client-js 1.6.0+).
Removed detailed explanation of client-side tool calling and potential pitfalls.
client-js only sends llm-function-call-result when the handler resolves with a defined value. A throw is swallowed by its .catch(), and null or undefined hits an `if (result == undefined) return`. Either way nothing goes back to the server, the call is never closed, and the bot waits on it for the rest of the session. register_function(timeout_secs=...) does not cover this. It only bounds time spent inside the server handler, and a handler that defers to the client returns immediately, which cancels the timeout task. Verified against pipecat 1.7.0: a no-op handler with timeout_secs=1 produces no FunctionCallResultFrame after 3 seconds. Nothing in this particular body can realistically throw, but this is the snippet people will copy for handlers that touch geolocation, local files, or fetch.
The tool takes no arguments, so `properties` stays empty: everything it
reports comes back from the browser in the result. Name the four fields
the client returns in the description instead, and point at
useClientTools.ts, so the two halves are readable together.
Verified in a live session that the LLM still calls it with `{}`.
jamsea
requested review from
markbackman
and
a lite review from Copilot
and removed request for
Copilot
August 12, 2026 04:04
client-js 1.10.0 -> 1.13.0, client-react 1.6.0 -> 1.8.1, daily-transport 1.6.5 -> 1.6.8, voice-ui-kit 0.11.0 -> 0.13.0. Verified on the new versions: client-js 1.13.0 still dispatches registered handlers from LLM_FUNCTION_CALL_IN_PROGRESS and keeps the `result == undefined` guard, so nothing this example relies on moved. voice-ui-kit 0.11 -> 0.13 is visually a non-event here. Re-ran the live session end to end afterwards: the tool call round tripped through the browser in 253 ms and the real user agent reached the LLM context.
There was a problem hiding this comment.
Pull request overview
Adds a push-to-talk example enhancement that demonstrates client-side LLM tool calling (browser-executed tool logic) using the current RTVI flow, along with documentation and dependency updates to support the new client tooling.
Changes:
- Advertise and register a
get_browser_infotool on the server, while delegating execution to the browser via RTVI function-call frames. - Add a client hook to handle the tool call in-browser and return browser-derived data to the bot.
- Update docs and client dependencies to reflect the new capability.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates the top-level examples list to mention client-side tool calling in push-to-talk. |
| push-to-talk/server/bot.py | Defines the get_browser_info tool schema, registers a no-op server handler, and configures RTVI function call reporting for client dispatch. |
| push-to-talk/README.md | Documents the new client-side tool calling behavior and use case. |
| push-to-talk/client/src/app/hooks/useClientTools.ts | Adds browser-side tool handler wiring for get_browser_info. |
| push-to-talk/client/src/app/components/App.tsx | Initializes the client-side tool handlers within the app. |
| push-to-talk/client/package.json | Updates/aligns Pipecat client and UI kit dependencies required for the example. |
| push-to-talk/client/package-lock.json | Locks updated dependency versions and adds transitive deps introduced by the upgrades. |
Files not reviewed (1)
- push-to-talk/client/package-lock.json: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Shows how to run an LLM tool call in the browser instead of on the server, using the current RTVI flow rather than the deprecated
RTVIProcessor.handle_function_call.The example tool is
get_browser_info. Ask the bot "what browser am I using?" and it reads back your real user agent, screen size, language, and time zone, none of which the server ever saw.How it works
LLMContext(tools=...)and registersrun_on_clientas its handler.llm-function-call-in-progress.run_on_clientreturns without callingparams.result_callback, which leaves the call open instead of answering it.llm-function-call-result.Two halves:
run_on_clientplus theCLIENT_TOOLwiring inserver/bot.py, andclient/src/app/hooks/useClientTools.ts.Why push-to-talk
sendClientMessage, so this completes the two-way picture in one example.function_call_timeout_secsdoes not cover deferred calls (the timeout task is cancelled as soon as the handler returns). Adding this to a multi-client example would hang the bot for every client that did not get the handler.Note on the report level
Client-run tools must opt in to
FULL.function_call_report_leveldefaults toNONE, which sends only atool_call_id, and a client cannot dispatch a call it cannot name. Other tools stay at the secure default.Verified end to end
Run against a live Daily session on 2026-08-12. Pipecat 1.7.0,
@pipecat-ai/client-js1.13.0,client-react1.8.1,daily-transport1.6.8,voice-ui-kit0.13.0. Asked the bot out loud for browser info:The LLM then reran with the browser's real data in context, which the server never had:
{"language": "en-US", "screen": "1728x1117", "timeZone": "Asia/Taipei", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ... Chrome/151.0.0.0 Safari/537.36"}Note
arguments {}: the tool takes no inputs, which is whypropertiesis empty.Barge-in while the bot is speaking also works: five clean interruptions in the same session, each starting a fresh user turn.