Skip to content

Add client-side tool calling to push-to-talk example - #241

Open
jamsea wants to merge 7 commits into
mainfrom
jh/push-to-talk-client-side-tools
Open

Add client-side tool calling to push-to-talk example#241
jamsea wants to merge 7 commits into
mainfrom
jh/push-to-talk-client-side-tools

Conversation

@jamsea

@jamsea jamsea commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

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

  1. The server advertises the tool in LLMContext(tools=...) and registers run_on_client as its handler.
  2. The LLM calls it. Pipecat tells the client via llm-function-call-in-progress.
  3. run_on_client returns without calling params.result_callback, which leaves the call open instead of answering it.
  4. The browser handler runs and returns a value, sent back as llm-function-call-result.
  5. Pipecat turns that into the call's result, adds it to context, and reruns the LLM.

Two halves: run_on_client plus the CLIENT_TOOL wiring in server/bot.py, and client/src/app/hooks/useClientTools.ts.

Why push-to-talk

  • It already demos client to server messaging via sendClientMessage, so this completes the two-way picture in one example.
  • It has a single client, which matters here. A tool the client never answers leaves the call open for the rest of the session, because function_call_timeout_secs does 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_level defaults to NONE, which sends only a tool_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-js 1.13.0, client-react 1.8.1, daily-transport 1.6.8, voice-ui-kit 0.13.0. Asked the bot out loud for browser info:

12:10:26.305  Calling function [get_browser_info:call_NJ8...] with arguments {}
12:10:26.306  Delegating get_browser_info to the client        <- no-op handler, call left open
12:10:26.310  FunctionCallInProgressFrame                       <- client told
12:10:26.558  FunctionCallResultFrame                           <- client answered, 253 ms

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 why properties is empty.

Barge-in while the bot is speaking also works: five clean interruptions in the same session, each starting a fresh user turn.

jamsea added 4 commits August 12, 2026 11:22
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
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.
@jamsea
jamsea marked this pull request as ready for review August 12, 2026 04:15
@jamsea
jamsea requested a lite review from Copilot August 12, 2026 04:15
@jamsea jamsea self-assigned this Aug 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_info tool 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.

Comment thread push-to-talk/client/src/app/hooks/useClientTools.ts
jamsea and others added 2 commits August 12, 2026 12:19
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants