UIWorker extends LLMContextWorker with the ability to see and act on whatever the user is looking at. It connects to the client GUI over the RTVI UI channel: it receives the screen as accessibility snapshots, reacts to the user’s UI events, and acts on the page by sending commands back to the client.
A UIWorker is the screen side of a voice/UI split. The voice agent (the main pipeline’s LLM) holds the conversation and does all the talking. When it needs something from the screen, it sends the UIWorker a job, and the worker answers with short data, never the whole page. The voice LLM’s context stays small and screen-unaware.
The smallest UIWorker is the class itself with an LLM:
respond, a screen-grounded LLM turn whose reply is the answer, and screen, which finds, checks, lists and acts on elements with the worker’s classifier and no LLM turn. Subclass it to add your own @job handlers, @ui_event handlers, or tools.
PipelineWorker connects a UIWorker to the client automatically when RTVI is enabled (the default), with no extra wiring.
The client streams the screen as
ui-snapshot messages and the worker drives
it with ui-command and ui-job-group messages. See The RTVI
Standard for the wire protocol and
Controlling the UI for a walkthrough.Configuration
Inheritsname and llm from LLMContextWorker, plus:
LLMContext | None
default:"None"
Optional pre-built
LLMContext. Seeded messages are part of the mutable
history and are cleared on each keep_history=False reset; put durable
instructions in the LLM’s system_instruction instead.BaseClassifier | None
default:"None"
Answers the worker’s small questions about the screen without an LLM turn:
which element the user means, whether something is true of the screen, which
elements match a description, and whether a UI event deserves a reply. When
omitted, the worker’s own
llm answers them through an
LLMClassifier, which works but
costs an LLM call per question. A
JevClassifier answers in about a
tenth of a second with calibrated probabilities.LLMAssistantAggregatorParams | None
default:"None"
Optional assistant-aggregator parameters, e.g. to enable context summarization
for
keep_history=True workers.bool
default:"True"
When
True (the default), append each UI event to the context as a
<ui_event> developer message. Override render_ui_event() to change the
content, or set False to disable.bool
default:"True"
When
True (the default), append the latest <ui_state> snapshot to the
context before every inference that starts a user turn (via the LLM’s
on_before_process_frame hook). Set False to inject manually with
inject_ui_state().bool
default:"False"
When
False (the default), the context is cleared at the start of every
respond job, so each turn sees only the current <ui_state> and query. When
True, history accumulates across jobs so the LLM can resolve multi-turn
references (“the next one”, “the Pro version”), at the cost of more tokens.
Pair with context summarization to prune history.str | None
default:"UI_STATE_PROMPT_GUIDE"
Wire-format guide appended to the LLM’s
system_instruction so it can parse
the <ui_state> and <ui_event> messages. Pass a string to override or
None to disable. Living in system_instruction, it survives context resets.Properties
Inherits all properties fromLLMContextWorker (including context, user_aggregator, assistant_aggregator, llm).
snapshot
None before the first one arrives. Read it with plain code when a job needs the page’s contents, rather than asking an LLM to parse <ui_state>.
selection
None when nothing is selected. A UISelection is a named tuple of ref (the snapshot ref of the element the selection is in) and text.
classifier
LLMClassifier built over the worker’s llm. Custom @job handlers can ask it their own questions.
current_job
respond job this worker is processing, or None when idle. Lets @tool methods inspect the in-flight job without threading the message through every call.
Built-in jobs
A requester sends a job to the worker by name, typically from a voice LLM tool:respond
Runs one screen-grounded LLM turn. The worker clears its context (unlesskeep_history=True), appends the query from payload["query"] as a user message, and runs its LLM with the latest <ui_state> injected. The reply the LLM writes is the answer: the job responds with {"answer": reply}, which the requester’s voice LLM phrases for the user.
A @tool on a subclass can answer instead by calling respond_to_job(), for example after acting on the page. A reply that comes with tool calls is treated as a preamble, not the answer.
respond jobs are single-flight: the worker runs one at a time, and the next waits until the current one is answered.
screen
Answers a question about the screen, or acts on it, without an LLM turn. The payload names anaction, a target and, for fill, a value. Every answer is short data, never the page. screen_tools() gives a voice LLM the tool that sends this job.
fill writes value into the element. A classifier failure answers the job with {"error": ...} and an error status.
screen_tools
UIWorker about the screen or act on it. One tool, screen(action, target, value), sends the worker’s screen job and returns its answer to the voice LLM as data. Its description teaches the LLM each action. Hand the tools to the voice LLM’s context:
The tool is not cancelled when the user interrupts. If the job fails, the tool returns
{"error": ...} to the voice LLM.
UI commands
These helpers send commands to the client. They are plain methods, not LLM tools: call them from a@job handler, a @ui_event handler, or a @tool body. Each is a wrapper around send_command with a typed payload model from pipecat.processors.frameworks.rtvi.models.
send_command
BusUICommandMessage; when RTVI is enabled, PipelineWorker translates it into an RTVIUICommandFrame on the pipeline. Client-side handlers subscribed to RTVIEvent.UICommand (or React’s useUICommandHandler) dispatch on the command name.
scroll_to
ref is a snapshot ref (e.g. "e42") from the latest snapshot.
highlight
select_text
start_offset..end_offset character sub-range when both are given.
click
disabled targets.
set_input_value
replace=True (the default) the field is overwritten; with replace=False the value is appended.
Screen questions
These methods answer questions about the latest snapshot. All butlist_elements() ask the worker’s classifier, so they cost no LLM turn when the classifier is a JevClassifier, and raise ClassifierError when it cannot answer. They are what the built-in screen job uses, and custom @job handlers can call them directly.
which_element
None when there is no snapshot, no named element, or no match with a probability of at least threshold.
check_screen
YesNoResult.
select_elements
{"ref", "label", "probability"} dicts, most likely first, or an empty list when nothing matches or there is no snapshot.
act
which_element(), then act on it. action is one of "click", "scroll_to", "highlight", "select_text" or "set_input_value"; value is the text to write for set_input_value. Returns the ref of the element acted on, or None when no element matched confidently. Raises ValueError for an unknown action.
should_respond
@ui_event handler to tell the few that do apart without an LLM turn. The question carries the event and the latest <ui_state>.
list_elements
role, name, its state tags and, for an input, its value, but no ref. Pass a role such as "checkbox" to list only those.
Responding to jobs
respond_to_job
respond job from a @tool, instead of letting the LLM’s reply answer it. The job responds with {"answer": answer} for the requester’s voice LLM to phrase, and a falsy answer completes the job with no answer, for a turn where the worker acted but has nothing to say. No-op when no job is in flight or it was already answered.
render_query
respond job. The default reads payload["query"]. Override to read a different payload shape; the returned string is appended to the context as a user message before the LLM runs.
render_ui_state
<ui_state> block (Playwright-MCP-style indented text with stable element refs). When the snapshot carries a text selection, a nested <selection ref="...">...</selection> block is appended so the LLM can resolve references like “this paragraph”. Returns an empty string if no snapshot has been received. Override to customize the rendered form.
inject_ui_state
<ui_state> block to the LLM context manually. No-op when no snapshot has been received. Use this when auto_inject_ui_state=False.
render_ui_event
<ui_event name="..."> tag with a JSON-encoded payload. Override to customize the injected content.
Job groups
AUIWorker fans work out to peer workers with the ordinary job_group() and request_job_group(). Every group it dispatches is reported to the client as it runs: a card when the group starts, a line per worker’s progress and completion, and the close when the group completes, whether normally, by cancellation or by timeout. There is no separate UI-specific call.
Give the group a label to title the card, and cancellable to say whether the client may stop it. Dispatch from a @job handler, so the voice LLM’s tool waits on the job while the user watches the progress:
request_job_group() instead to start the work in the background and answer right away.
Handling UI events
@ui_event
PipecatClient.sendUIEvent(event, payload), the matching handler runs in its own task. The handler receives the BusUIEventMessage (read message.payload for the event data).
Two handlers can’t share the same event name on the same subclass. Overrides
in subclasses take precedence over base-class definitions.
ReplyToolMixin
ReplyToolMixin adds a single reply tool to a UIWorker subclass (class MyUIWorker(ReplyToolMixin, UIWorker)). The tool takes a required spoken answer plus optional scroll_to, highlight, select_text, fills and click actions, applies the actions in that order, and then speaks the answer verbatim through the requester’s TTS.