Compare commits

..

64 Commits

Author SHA1 Message Date
Mark Backman
9054912dfb Update to add_workers 2026-05-21 23:20:40 -04:00
Mark Backman
0b9500aae4 Match shopping-list client styling to the other UI demos
Restyle from a bespoke dark theme to the light theme the other UI demos
share: the canonical :root tokens (--border, --muted, --highlight), the
#fafafa/#18181b body, the sticky white header with the light/red Connect
button, the fixed bottom-right #status toast, and the amber
ui-highlight-pulse keyframe. index.html drops the custom topbar wrapper for
the standard <header> plus a standalone #status element.
2026-05-21 23:20:40 -04:00
Mark Backman
10b8feb9ea Add shopping-list UIWorker example (bridge-free voice + UI)
Demonstrates the 'every input acts, may speak' pattern without bridging: a
standard voice pipeline (STT → LLM → TTS) whose LLM only converses, plus a
separate UIWorker that does all the list work. The voice pipeline's user
aggregator fires on_user_turn_stopped each turn and dispatches the transcript
to the UIWorker as a respond job (a bus message); the UIWorker reads the
auto-injected <ui_state> snapshot and drives the list silently via add_item /
set_checked / remove_item commands (plus the standard highlight). Items are
checkboxes whose label and checked state the snapshot exposes.

Includes a vanilla-JS client following the existing UI-demo client style.
2026-05-21 23:20:40 -04:00
Mark Backman
1c94feaaff Inject <ui_state> via the LLM's on_before_process_frame hook
Move <ui_state> snapshot injection out of respond_with_llm into a
cross-cutting on_before_process_frame handler on the UIWorker's LLM, so it
appends the current snapshot to the context the request is built from, just
before each inference. Injection is gated to the user-turn-initiating
inference so a tool-calling turn never stacks duplicate <ui_state> blocks;
respond_with_llm no longer injects manually.

Also drop the bridged parameter from UIWorker: there is no viable way to
bridge a UIWorker between workers — a shared, teed context would be polluted
by the injection, and per-worker turn detection off teed frames isn't
supported. Other workers keep their PipelineWorker bridging.
2026-05-21 23:20:40 -04:00
Mark Backman
950fc10f05 Add document-review UIWorker example
Synthesis example: a ReplyToolMixin UIWorker adds a start_review tool that fans
out to clarity/tone peers via start_user_job_group, translates each reviewer
response into an add_note command in on_job_response, handles a client
note_click event via @on_ui_event, and keeps history across turns.
2026-05-21 23:20:40 -04:00
Mark Backman
07725429b2 Add async-tasks UIWorker example
A UIWorker with a custom reply tool fans research out to three BaseWorker peers
via start_user_job_group; their progress streams to the client as ui-task cards
and the user can cancel a group mid-flight.
2026-05-21 23:20:40 -04:00
Mark Backman
6b0e204d66 Add form-fill UIWorker example
A ReplyToolMixin UIWorker that fills inputs (fills) and toggles checkboxes /
presses submit (click) by voice — the state-changing half of the standard
action set.
2026-05-21 23:20:40 -04:00
Mark Backman
f826da9ac9 Add deixis UIWorker example
A ReplyToolMixin UIWorker that grounds in the user's text selection (the
<selection> block in the snapshot) and points back via select_text — both
directions of deictic reference.
2026-05-21 23:20:40 -04:00
Mark Backman
81b956d963 Add pointing UIWorker example
The voice LLM delegates to a ReplyToolMixin UIWorker that scrolls offscreen
items into view and highlights the phones it names — exercising the scroll_to /
highlight UI commands and the [offscreen] state tag.
2026-05-21 23:20:40 -04:00
Mark Backman
2254a8d0a2 Add hello-snapshot UIWorker example
Smallest UIWorker demo: a voice LLM in the main pipeline delegates
screen-relevant utterances to a UIWorker via a respond job; the UIWorker
auto-injects the current <ui_state> and answers grounded in what's on screen.
Includes a vanilla-JS client that streams accessibility snapshots over RTVI.
2026-05-21 23:20:40 -04:00
Mark Backman
f1f5a986e8 Add UIWorker
UIWorker is an LLMContextWorker that observes and drives a client GUI over the
RTVI UI channel: it stores accessibility snapshots, auto-injects <ui_state> at
the start of each respond job, dispatches client events to @on_ui_event
handlers, sends UI commands back to the client, and surfaces fan-out work as
cancellable task cards via user_job_group(). The optional ReplyToolMixin exposes
a bundled reply tool.

The prompt_guide parameter auto-appends the UI wire-format guide to the LLM's
system instruction (default UI_STATE_PROMPT_GUIDE; override with a string or
disable with None), so the LLM can parse the injected <ui_state> / <ui_event>
messages without the app concatenating the guide by hand.
2026-05-21 23:20:40 -04:00
Mark Backman
02667a7255 Add native RTVI⇄bus UI bridge to PipelineWorker
When RTVI is enabled, PipelineWorker now republishes inbound ui-event /
ui-snapshot / ui-cancel-task messages onto the bus as a broadcast
BusUIEventMessage, and translates outbound BusUICommandMessage / BusUITask*
carriers into the matching RTVI frames. This lets a UIWorker on the bus observe
and drive the client UI with no decorator or manual wiring; when no UIWorker is
present the events are simply unconsumed.

The BusUI* carriers live in the bus layer so both pipeline and workers can
reference them without an import cycle.
2026-05-21 23:03:37 -04:00
Mark Backman
ee3d1128ec Add LLMService.append_system_instruction()
Composes durable text onto a user-provided system instruction (alongside the
turn-completion and async-tool-cancellation addons) so it is prepended on every
inference and survives context-message resets. The user's base prompt is now
snapshotted once and the effective instruction is always rebuilt from it,
replacing the prior lazy capture/restore logic with a single invariant.
2026-05-21 23:03:37 -04:00
Aleix Conchillo Flaqué
e8ec7c585f Rename PipelineRunner.add_worker() to variadic add_workers(*workers)
Lets callers register multiple workers in a single call instead of
awaiting add_worker() repeatedly. Updates all examples, docs, tests,
and proxy worker docstrings to use the new API.
2026-05-21 19:46:53 -07:00
Aleix Conchillo Flaqué
f91179a640 Forward active from PipelineWorker through to BaseWorker
PipelineWorker.__init__ was only forwarding `name` to BaseWorker, so
the `active` flag (the other BaseWorker constructor arg) wasn't
reachable from PipelineWorker callers. Add `active: bool = True` to
the signature and pass it through.
2026-05-21 19:07:13 -07:00
Aleix Conchillo Flaqué
e85f3fe606 update uv.lock 2026-05-21 19:07:13 -07:00
Aleix Conchillo Flaqué
d07ba562eb Separate bus messages from pipeline frames
BusMessage was a mixin tacked onto DataFrame / SystemFrame so the bus
could reuse the frame priority machinery. That made every bus message
also a Frame, which is misleading — bus messages travel on the bus, not
through pipelines. If a worker actually needs to ship a frame, it wraps
it in BusFrameMessage.

BusMessage is now a plain dataclass base carrying source/target.
BusDataMessage and BusSystemMessage are empty subclasses that exist
only as priority markers. The bus router and the priority queue check
``isinstance(item, BusSystemMessage)`` directly instead of
``isinstance(item, SystemFrame)``.

The serializer test that round-tripped DataFrame.name (a non-init
field) is rewritten against a local _MessageWithNonInit(BusDataMessage)
subclass so the serializer's init=False path stays covered.
2026-05-21 19:07:13 -07:00
Aleix Conchillo Flaqué
b03247f360 Rename BaseTask → BaseWorker and reserve "task" for asyncio
Replaces every "task" identifier that referred to the BaseTask
abstraction with "worker". Asyncio task plumbing (asyncio.Task,
BaseTaskManager, TaskManager, create_task, cancel_task, etc.) stays
untouched. Highlights:

- Classes: BaseTask → BaseWorker, PipelineTask → PipelineWorker,
  LLMTask → LLMWorker, LLMContextTask → LLMContextWorker, TaskBus →
  WorkerBus, TaskRegistry → WorkerRegistry, TaskActivationArgs →
  WorkerActivationArgs, TaskReadyData → WorkerReadyData,
  TaskRegistryEntry → WorkerRegistryEntry, TaskObserver →
  WorkerObserver, all Bus*TaskMessage → Bus*WorkerMessage,
  BusAddTaskMessage.task field → worker, BusWorkerRegistryMessage.tasks
  field → workers.
- Methods/decorators: activate_task → activate_worker, deactivate_task
  → deactivate_worker, add_task → add_worker, watch_task →
  watch_worker, @task_ready → @worker_ready, setup_pipeline_task hook
  → setup_pipeline_worker.
- Params/fields: FrameProcessorSetup.pipeline_task and
  FunctionCallParams.pipeline_task → pipeline_worker. Parameter names
  like task_name → worker_name; spawn/run accept worker:.
- Files: pipeline/base_task.py → base_worker.py, pipeline/task.py →
  worker.py (plus a re-export shim at pipeline/task.py),
  task_observer.py → worker_observer.py, task_ready_decorator.py →
  worker_ready_decorator.py, pipecat.tasks → pipecat.workers,
  llm_task.py → llm_worker.py, llm_context_task.py →
  llm_context_worker.py, examples/multi-task → examples/multi-worker.

Back-compat:
- PipelineTask kept as a deprecated subclass of PipelineWorker that
  warns on construction.
- pipecat.pipeline.task re-exports PipelineWorker/PipelineTask/etc. so
  existing user imports keep working.
- FrameProcessor.pipeline_task kept as a deprecated property that
  forwards to pipeline_worker.

Local variables in examples that hold a worker (task = PipelineTask(...))
are renamed to worker = PipelineWorker(...). Asyncio-task locals
(runner_task, etc.) are preserved.
2026-05-21 19:07:13 -07:00
Aleix Conchillo Flaqué
b9aed0d673 Rename BaseTask.send_error to send_bus_error_message
Symmetric with send_bus_message; "send_bus_error" on its own reads
ambiguously (sounds like an error about the bus, à la SIGBUS) and the
underlying types are BusTaskErrorMessage / BusTaskLocalErrorMessage,
so keeping "_message" in the name matches what's actually sent.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
d8947c68a9 Rename BaseTask.send_message to send_bus_message
Mirrors on_bus_message and makes it explicit that the call goes out on
the task bus, not on a transport (transports have their own
send_message for client/peer messaging).
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
373894fc65 Fold BaseTask.handoff_to into activate_task(deactivate_self=...)
BaseTask.handoff_to was just deactivate_self + activate_task. Remove
it and add a deactivate_self flag on activate_task instead, so there's
one entry point for activating another task.

LLMTask now overrides activate_task (mirroring its end() override) to
keep the messages / result_callback hooks that finish an in-progress
tool call before the target is activated. All multi-task examples and
unit tests switch to the new call.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
e8bbb5ee09 Add setup_pipeline_runner hook to PIPECAT_SETUP_FILES
PipelineRunner now picks up an async setup_pipeline_runner(runner) hook
from the same PIPECAT_SETUP_FILES env var that PipelineTask already uses
for setup_pipeline_task. Previously the runner used a separate
PIPECAT_RUNNER_SETUP_FILES variable and a setup_runner function — both
are removed.

A new _setup_files module hosts the loader for both hooks and caches
each setup file's module so a single file defining both hooks (e.g. a
debugger that registers a runner-level task in one hook and a per-task
observer in the other) sees its module-level state preserved across
invocations.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
a2e58044f2 update pyproject.toml and uv.lock 2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
8867426a97 Document sensor-controller example in the multi-task README
Add a Local-section entry with the running instructions, example
questions, and architecture diagram for the new sensor-controller
example.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
d984393213 Make local-handoff builder functions public
Rename ``_build_greeter`` / ``_build_support`` to ``build_greeter`` /
``build_support`` to match the convention used by other multi-task
examples (e.g. ``build_sensor_controller``). They're public factories
the example exposes; the leading underscore was misleading.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
959fb831f1 Drop redundant name strings from create_task calls in bus + proxy
``BaseObject.create_task`` already auto-names the task based on the
coroutine; the explicit ``f"{self}::..."`` strings duplicated that
default and made the call sites noisier. Remove them.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
410190dabb Add sensor-controller multi-task example
A voice agent talking to a worker that owns a simulated temperature
sensor. Demonstrates two ``PipelineTask`` instances side by side
communicating purely via ``BusJobRequestMessage`` /
``BusJobResponseMessage`` — the worker is a plain ``PipelineTask``
(no ``LLMTask`` subclassing, not bridged) whose pipeline runs both an
autonomous sensor tick loop and its own tool-calling LLM:

    SensorReader -> SensorStats -> user_agg -> llm -> assistant_agg

The voice agent's LLM has a single tool, ``ask_controller(question)``,
that forwards the user's request verbatim to the worker and speaks
back the controller's reply. The worker LLM has direct tools to read
the current temperature, inspect rolling stats, set the target, or
change the response rate; the sensor simulation drifts toward the
target with a first-order lag plus Gaussian noise.

Job responses are paired with completed LLM turns via the assistant
aggregator's ``on_assistant_turn_stopped`` event, skipping empty
turn-stopped events that fire between a tool call and its result.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
f22350ce2f Use symmetric spawn-then-run() pattern in multi-task examples
Switch every example to ``await runner.spawn(task)`` followed by
``await runner.run()`` (no task argument), and ``await runner.cancel()``
on client-disconnected instead of ``await task.cancel()``. This makes
the main pipeline task look the same as the worker / proxy tasks
spawned alongside it, and lets ``runner.cancel()`` drive a uniform
shutdown across every root task on the bus.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
5f1b91bb89 Clarify PipelineRunner.run() docstring for the no-task form
Spell out that spawned tasks finishing on their own does not unblock
``runner.run()`` when called without a ``task`` argument. The form is
for hosts (e.g. FastAPI servers) that have no single "main" pipeline
and want to stay up across many spawned sessions; callers who want
the runner to finish when a specific pipeline finishes should pass
that pipeline as ``task``.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
cd22742e10 Default WebSocketProxyClientTask to active=False
``on_activated`` on this task opens the upstream WebSocket connection,
which is almost always something the caller wants to trigger
explicitly (e.g. on local-client-connected). With the BaseTask default
of ``active=True`` the connection was opened twice: once when the task
auto-activated at start, and once again when the caller's
``activate_task("proxy")`` re-fired ``on_activated``. The result on
the remote side was two ``PipelineRunner`` instances per session
instead of one.

Default to ``active=False`` so the activation is a deliberate signal;
pass ``active=True`` explicitly to restore the eager-connect behavior.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
9ecb00d097 Skip pgmq/redis lazy-import tests when their extras are not installed
``test_pgmq_bus_lazy_import`` and ``test_redis_bus_lazy_import``
import ``pipecat.bus.network.pgmq`` / ``redis`` directly, which raises
when the optional ``pgmq`` / ``redis`` packages are missing. Gate each
test with ``@unittest.skipUnless`` on a top-level probe of the
underlying package so they're skipped (not errored) in environments
without the extras. ``test_unknown_attribute_raises`` is unaffected.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
79ae9740cc Skip pgmq/redis bus tests when their extras are not installed
The PGMQ and Redis bus modules raise an ``Exception`` at import time
when the optional ``pgmq`` / ``redis`` packages are missing, which broke
``pytest`` collection in environments without those extras (e.g. CI
that uses ``--no-extra gstreamer --no-extra local``). Wrap the imports
in ``try/except`` and ``raise unittest.SkipTest`` so the whole test
module is skipped cleanly instead of failing collection.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
df704a34f1 Move _wait_tasks_ready into the job-group internals section in BaseTask
``_wait_tasks_ready`` is only called from
``create_job_group_and_request_job``, so it belongs with the other
job-group internals (``_create_job_group``, ``_send_job_request``,
``_task_timeout``, ...) rather than next to the task-readiness
helpers (``_register_ready``, ``_on_watched_task_ready``).
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
7dc2b41412 Drive task end/cancel shutdown from BaseTask by default
``BaseTask._handle_task_end`` and ``_handle_task_cancel`` now call
``stop()`` after propagating to children, so bus-only subclasses
(``WebSocketProxyClientTask``, ``WebSocketProxyServerTask``, custom
worker tasks like ``CodeWorker``) don't need to override these
handlers just to set ``_finished_event``.

Children-propagation is extracted into ``_propagate_end_to_children``
and ``_propagate_cancel_to_children`` so ``PipelineTask`` can call
them directly without invoking ``stop()`` prematurely — the pipeline
still drives its own shutdown through the ``EndFrame`` / ``CancelFrame``
path, which triggers ``on_pipeline_finished`` and ``stop()`` after the
pipeline drains.

Drop the now-redundant overrides from the WebSocket proxy tasks.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
4d9e258e55 Collapse _pipeline_finished_event into BaseTask._finished_event
PipelineTask had its own ``_pipeline_finished_event`` that signalled
"pipeline run has truly finished" — the same role ``BaseTask._finished_event``
plays for bus-only tasks. They were two events with the same intent.

Set ``_finished_event`` directly when the pipeline-end frame propagates
through the sink, drop the now-redundant field, and drop the
``clear()`` after wait so the event stays set for the lifetime of the
task. As a side-effect, ``await pipeline_task.wait()`` from outside now
resolves at the moment the pipeline finishes, matching the semantics
of bus-only tasks.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
de1bd7cb7e code-assistant: work around CancelledError swallow in ClaudeSDKClient
claude_agent_sdk's _AsyncioTaskHandle.wait() uses
`with suppress(asyncio.CancelledError)` to silence the inner read
task's expected cancellation, but it also swallows the outer task's
cancellation if it lands on the same await — causing cancel_task to
time out.

Bypass `async with ClaudeSDKClient` and drive connect/disconnect
ourselves so disconnect() runs in a finally where the outer
CancelledError has already been raised and suspended by Python's
exception machinery, out of reach of the SDK's suppress.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
a5bb9f65de Fix LLMTask._finish_function_call to bypass deferral
self.queue_frame would defer the LLMMessagesAppendFrame because
_finish_function_call always runs inside a tool call. The subsequent
_flush_pipeline() then returned before the goodbye/handoff LLM output
was actually delivered. Use super().queue_frame to push the frame
straight into the pipeline, matching the pattern used in
_flush_pipeline().
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
402cf8dade Port multi-task unit tests from pipecat-subagents
Brings over 215 tests across 15 files covering the new
multi-task framework: BaseTask / PipelineTask bus lifecycle,
job RPC and job groups, the bus message hierarchy and serializers,
TaskBus + AsyncQueueBus + RedisBus + PgmqBus (with direct and
isolated backends), TaskRegistry, the BusBridgeProcessor, the
WebSocket proxy tasks, the LLMTask deferral logic, and the
PipelineRunner spawn-and-attach flow.
2026-05-21 10:13:21 -07:00
Jon Taylor
d757d8d06d Split PgmqBus into orchestrator + pluggable backends
Move the wire-side of PGMQ operations into a new
``pipecat.bus.network.pgmq_backends`` module with a ``PgmqBackend``
Protocol, a ``DirectPgmqBackend`` (peers discovered by queue prefix),
and an ``IsolatedPgmqBackend`` (SECURITY DEFINER ``public.bus_*``
wrappers over an asyncpg pool). ``PgmqBus`` now delegates join,
publish, read, archive, and leave to the configured backend.

Construct ``PgmqBus`` with either ``pgmq=PGMQueue`` (uses
``DirectPgmqBackend``) or ``backend=PgmqBackend`` (any backend); the
two are mutually exclusive.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
a63abc41b6 Add README and env.example for multi-task examples
Adapts the pipecat-subagents `examples/README.md` to the new
layout (`multi-task/` umbrella, `local-handoff/`, `distributed-handoff/`,
`remote-proxy-assistant/`, `parallel-debate/`, `code-assistant/`),
updates the agent→task / job-RPC vocabulary, drops the
single-agent and llm-and-flows examples (gone in the port), and
adds a new section for the PGMQ handoff transport.
2026-05-21 10:13:21 -07:00
Aleix Conchillo Flaqué
4c5fb85856 Add pgmq and redis extras for the distributed bus implementations
`pipecat.bus.network.pgmq` and `pipecat.bus.network.redis` need
optional dependencies. Adding `pgmq` and `redis` extras so users
can `pip install pipecat-ai[pgmq]` / `pip install pipecat-ai[redis]`
to opt in.
2026-05-21 10:13:18 -07:00
Aleix Conchillo Flaqué
4fbeb5fbcb Add remote-proxy-assistant example
Demonstrates the WebSocket proxy tasks: a local `main.py` voice
bot uses `WebSocketProxyClientTask` to forward bus messages
(including `BusFrameMessage`s) to a remote `assistant.py`
FastAPI server. Each incoming connection spawns a
`WebSocketProxyServerTask` plus an `LLMTask` assistant on a
per-session `PipelineRunner`.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
4509caa724 Add distributed-handoff examples (redis and pgmq)
Two transports of the same shape: a main task that hosts the
voice pipeline plus a network-backed `TaskBus` (`RedisBus` or
`PgmqBus`), and a standalone `llm.py` worker process for the
greeter / support LLM. Workers connect to the same bus channel,
register on the shared `TaskRegistry`, and the main task waits
on `runner.registry.watch("greeter", ...)` before sending the
welcome activation so it doesn't fire before the worker is up.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
0f7211d072 Add parallel-debate example
A voice moderator that fans out a debate topic to three worker
tasks (advocate, critic, analyst) via `task.job_group(...)`,
then synthesizes their replies. Workers are `LLMContextTask`s
that keep their own conversation context across rounds and use
the assistant-aggregator's `on_assistant_turn_stopped` event
to ship the completed turn back as a job response.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
7c4294b7f6 Add local-handoff-two-agents-tts example
Variant of the local handoff example with per-task TTS voices.
Each child task wraps the LLM with its own `CartesiaTTSService`
in a custom pipeline override, so the main task has no TTS and
audio comes from whichever child is active over the bus.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
6964686808 Add code-assistant example
Voice code assistant that dispatches questions to a Claude Agent
SDK worker. The main task runs the voice pipeline (STT + LLM + TTS)
and an `ask_code` direct function. `CodeWorker` is a bus-only
`BaseTask` spawned on the runner: it accepts `@job`-style
requests through the bus, queues them onto an asyncio queue, and
runs them sequentially through a persistent Claude SDK session so
follow-ups share context. The example shows the job-RPC surface
(`task.job("code_worker", ...)`), bus-only tasks (no pipeline),
and the `pipeline_task` field on `FunctionCallParams`.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
f364c088cf Add local-handoff-two-agents example
Two LLM tasks (greeter and support) handing off to each other over
the local `AsyncQueueBus`. The main task owns the transport
pipeline (STT, TTS, transport I/O) and the child tasks each run
their own LLM behind a `BusBridgeProcessor`. Each child uses
`bridged=()` so `PipelineTask` auto-wraps its pipeline with
the bus edge processors, and `transfer_to_agent` / `end_conversation`
tools demonstrate `handoff_to(...)` and `end(...)`.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
42204c4d0f Fix pyright errors in new bus/task/proxy code
- `TaskBus._router_task`: cast the narrowed `SystemFrame` back
  to `BusMessage` for the subscriber callback.
- `bus.network.__init__`: expose `PgmqBus` / `RedisBus` to
  the type-checker via a TYPE_CHECKING block so `__all__` is
  satisfied; runtime path still goes through `__getattr__`.
- `RedisBus`: subscribe through a local before assigning
  `self._pubsub`, and `assert self._pubsub is not None` in
  the reader loop.
- `BaseTask.on_job_error` accepts
  `BusJobResponseMessage | BusJobResponseUrgentMessage` to match
  what is dispatched.
- `JobGroupContext.__aexit__` / `JobContext.__aexit__`: assert
  `self._group is not None` before `wait()`.
- `@task_ready` collector: type handlers dict as `dict[str, Callable]`
  so the `.__name__` read on a duplicate handler typechecks.
- WebSocket proxy client/server: assert the socket is set in
  `_receive_loop`, and decode `str` payloads to bytes before
  handing them to the serializer.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
5f86e39038 Port WebSocket proxies to the new BaseTask API
`WebSocketProxyServerAgent` / `WebSocketProxyClientAgent` are
renamed to `WebSocketProxyServerTask` / `WebSocketProxyClientTask`
and updated for the post-refactor surface:

- Drop `bus=` from the constructor; the bus arrives via
  `BaseTask.attach` from the runner.
- Constructor params `agent_name` / `remote_agent_name` /
  `local_agent_name` → `task_name` / `remote_task_name` /
  `local_task_name` (matching `BusBridgeProcessor`).
- Move setup logic from the now-removed `on_ready` hook into
  `start()`; replace `_stop()` overrides with `stop()`.
- Add `_handle_task_end` / `_handle_task_cancel` overrides that
  set `_finished_event` so `PipelineRunner._cancel_spawned_tasks`
  can drive these bus-only tasks to a clean exit.
- Update the registry-message field reference
  (`agents=`/`message.agents` → `tasks=`/`message.tasks`)
  and `TaskReadyData.task_name` access.
- Tighten the server's `_send_ws` exception handling to only
  catch `WebSocketDisconnect`.
- Update install hints (`pipecat-ai[websockets-base]` for the
  client, `starlette` for the server) and refresh docstrings/
  examples to use `runner.spawn(...)`.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
86f8f137a8 Sweep agent->task and task->job in docstrings and identifiers
Cleans up leftover "agent" terminology in module/class/method
docstrings across `pipecat.bus`, `pipecat.registry`,
`pipecat.pipeline`, and `pipecat.tasks.llm`, and renames
job-RPC phrasing ("task request", "task identifier",
"task group execution") to use "job" consistently.

API-visible changes:

- `BusBridgeProcessor(agent_name=, target_agent=)` → `task_name=` /
  `target_task=`.
- `@task_ready` decorator's internal marker
  `fn.agent_ready_name` → `fn.task_ready_name`.
- `@tool` decorator's internal marker
  `fn.is_agent_tool` → `fn.is_llm_tool`.
- `PIPECAT_SUBAGENTS_SETUP_FILES` env var →
  `PIPECAT_RUNNER_SETUP_FILES`.
- pgmq/redis bus install hints point at `pipecat-ai\[extra\]`
  rather than the old `pipecat-ai-subagents\[extra\]` package.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
e546471bef Rename _handle_task_* job dispatchers to _handle_job_*
Fixes a name collision where `_handle_task_cancel` was defined
twice — once for `BusCancelTaskMessage` (task lifecycle) and
once for `BusJobCancelMessage` (job RPC) — the second silently
shadowing the first. Job-side dispatchers are now consistently
named `_handle_job_*` and the internal helpers
`_run_task_handler` / `_send_task_request` become
`_run_job_handler` / `_send_job_request`. Task-lifecycle
handlers (`_handle_task_end`, `_handle_task_cancel`,
`_handle_task_activate`, `_handle_task_deactivate`,
`_handle_task_error`) keep their names.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
6d87765648 LLMTask/LLMContextTask: fix LLMService type 2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
922293ae76 Spawn the main task before setup so attach happens uniformly
`PipelineRunner.run(task)` now calls `spawn(task)` first (which
runs `task.attach()`) and lets `_setup_session` start every
registered entry — main and pre-spawned — through the same path,
instead of relying on `spawn`'s post-running fast-path to start
the main task after setup. The two-branch wait stays for the
`task is None` case but reads the runner_task directly off the
freshly-spawned entry.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
eb4f0ac1ae Add changelog for #4493 2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
7506af5861 Replace set_registry with attach(*, registry, bus) on BaseTask
`BaseTask` no longer takes `bus=` in its constructor. Instead
the runner now hands both the registry and the bus to a task via
`task.attach(registry=..., bus=...)` (called from
`PipelineRunner.spawn()`), and `bus` / `registry` are
properties that raise if accessed before attach. `PipelineTask`,
`LLMTask`, and `LLMContextTask` lose their `bus=` parameters
to match, and `_BusEdgeProcessor` now stores only a task
reference and reads `task.bus` lazily so bridged pipelines work
even though the bus isn't known at construction time.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
ef806163b2 Tighten the pipeline_task contract for processors and tools
`FrameProcessorSetup.pipeline_task` is now mandatory and
`FrameProcessor.pipeline_task` raises if accessed before setup
instead of returning `None`. `FunctionCallParams` gains a
required `pipeline_task` field and `LLMService._run_function_call`
populates it (plus reads `app_resources` directly off the
pipeline task). Tests that build a processor or
`FunctionCallParams` outside a real pipeline stub it with a
`SimpleNamespace`.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
7d28c46a5d Add tasks package with LLMTask, LLMContextTask, and proxy stubs
Adds `pipecat.tasks.llm` with `LLMTask` (LLM pipeline + `@tool`
collection + tool-call deferral via `PipelineFlushFrame`),
`LLMContextTask` (LLM + `LLMContextAggregatorPair`), and the
`@tool` decorator. Also includes `pipecat.tasks.proxy.websocket`
client/server stubs that need a follow-up port to the new
`BaseTask` lifecycle.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
befaa9ff27 Rewrite PipelineRunner around bus + spawn
`PipelineRunner` now owns the shared `TaskBus` and
`TaskRegistry` and runs all tasks (the main one plus any
spawned ones) through a unified `_start_task` / `_run_task`
background-task path. Adds `spawn(task)` for fire-and-forget
task registration, threads `end()` / `cancel()` through
`BusEndTaskMessage` / `BusCancelTaskMessage` to all root
tasks, and broadcasts/handles `BusTaskRegistryMessage` for
remote-runner discovery. The runner now wires its own
`TaskManager` via `super().setup(...)` so internal
`create_task` calls go through `BaseObject`.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
b5c757ab85 Make PipelineTask inherit BaseTask and support bridged pipelines
`PipelineTask` now extends `BaseTask` so every pipeline task is
also a bus participant. Adds optional `bus`, `bridged`, and
`exclude_frames` parameters: when `bridged` is set, the user's
pipeline is wrapped with `_BusEdgeProcessor` source/sink edges so
frames are mirrored onto the bus. Bridges pipeline lifecycle
events to `start()`/`stop()`, overrides `_handle_task_end` /
`_handle_task_cancel` to drive the pipeline shutdown, subscribes
to the bus in setup, and exposes the `bridged` property to the
registry. Moves `PipelineTaskParams` here and updates the
matching test import.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
6a738bd3a0 Replace BasePipelineTask with BaseTask
Drops the old abstract `BasePipelineTask` and replaces it with
`BaseTask` — the common base for any runtime task. `BaseTask`
subscribes to a `TaskBus`, participates in the shared
`TaskRegistry`, handles activation / deactivation, end / cancel,
and the full `@job` RPC surface (request_job, job, job_group,
send_job_response / update / stream_*, etc.). It ships a default
`run()` for bus-only tasks; subclasses with their own runtime
(e.g. `PipelineTask`) override it.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
c0b2a8c572 Add job context and decorators
Adds `JobContext` / `JobGroupContext` async context managers,
the `JobGroup` / `JobGroupEvent` / `JobGroupResponse` /
`JobGroupError` types, the `@job` decorator (with collector),
and the `@task_ready` decorator (with collector). These power
the bus-driven job RPC between tasks.
2026-05-21 10:12:51 -07:00
Jon Taylor
7e2055b7d0 Add PgmqBus for distributed agents
Adds ``pipecat.bus.network.pgmq.PgmqBus``, a PGMQ-backed
:class:`TaskBus` adapter that implements pub/sub fan-out over
PGMQ's point-to-point queue semantics. Each bus instance owns its
own queue, broadcasts on publish to peers discovered by channel
prefix, and long-polls its queue to dispatch received messages
to local subscribers.

Requires the optional ``pgmq`` extra
(``pip install pipecat-ai[pgmq]``).
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
5d94506265 Add task bus package
Introduces `TaskBus`, the in-process `AsyncQueueBus`, the bus
message hierarchy (lifecycle, jobs, frames, registry), a
priority-aware bus queue, the `BusSubscriber` mixin, and the
`BusBridgeProcessor` / internal `_BusEdgeProcessor` used to
exchange frames between a local pipeline and the bus.
2026-05-21 10:12:51 -07:00
Aleix Conchillo Flaqué
30df8e4ca5 Add task registry package
Introduces `TaskRegistry` and the supporting `TaskReadyData`,
`TaskErrorData`, and `TaskRegistryEntry` dataclasses used to track
local and remote tasks discovered through the bus.
2026-05-21 10:12:51 -07:00
483 changed files with 37025 additions and 3954 deletions

View File

@@ -41,7 +41,9 @@ jobs:
--extra google \
--extra langchain \
--extra livekit \
--extra pgmq \
--extra piper \
--extra redis \
--extra runner \
--extra sagemaker \
--extra tracing \

View File

@@ -45,7 +45,9 @@ jobs:
--extra google \
--extra langchain \
--extra livekit \
--extra pgmq \
--extra piper \
--extra redis \
--extra runner \
--extra sagemaker \
--extra tracing \

1
changelog/4493.added.md Normal file
View File

@@ -0,0 +1 @@
- Added `pipecat.workers`, a worker-based agent framework folded in from the standalone `pipecat-subagents` package. Workers inherit from `BaseWorker`, share a `WorkerBus`, register in a `WorkerRegistry`, and exchange typed work via `@job` handlers. `LLMWorker` and `LLMContextWorker` provide ready-made LLM-driven workers. `PipelineRunner.spawn(worker)` registers fire-and-forget workers alongside the main pipeline worker.

View File

@@ -0,0 +1 @@
- ⚠️ `FrameProcessorSetup.pipeline_worker` and `FunctionCallParams.pipeline_worker` are now mandatory fields, and `FrameProcessor.pipeline_worker` raises if read before `setup()` instead of returning `None`. Real-world code (frame processors set up by `PipelineWorker`, tool handlers invoked by `LLMService`) is unaffected; only callers that construct these dataclasses by hand (typically tests) now have to supply a `pipeline_worker` reference.

View File

@@ -0,0 +1 @@
- `PipelineWorker` now inherits from `BaseWorker`, so every pipeline worker is also a bus participant. It accepts a new optional `bridged=()` parameter that auto-wraps the pipeline with bus edge processors, letting the worker exchange frames with other bridged workers over the shared `WorkerBus`. The bus is supplied by `PipelineRunner` via `worker.attach(registry=..., bus=...)` instead of through the constructor.

View File

@@ -0,0 +1 @@
- Added `LLMService.append_system_instruction(...)`, which composes durable text onto a user-provided system instruction (alongside the turn-completion and async-tool-cancellation instructions) so it is prepended on every inference and survives context-message resets.

3
changelog/xxxx.added.md Normal file
View File

@@ -0,0 +1,3 @@
- Added `pipecat.workers.ui.UIWorker`, an `LLMContextWorker` that observes and drives a client GUI over the RTVI UI channel: it stores live accessibility snapshots, auto-injects `<ui_state>` into the LLM context before every inference (via the LLM's `on_before_process_frame` hook), dispatches client events to `@on_ui_event` handlers, and sends UI commands (`scroll_to`, `highlight`, `select_text`, `click`, `set_input_value`) back to the client. The optional `ReplyToolMixin` exposes a bundled `reply` tool, and `user_job_group(...)` surfaces fan-out work to the client as cancellable task cards. A native RTVI⇄bus UI bridge is built into `PipelineWorker` (active whenever RTVI is enabled), so no decorator or manual wiring is needed: inbound UI messages are broadcast on the bus as `BusUIEventMessage`, and outbound `BusUICommandMessage` / `BusUITask*` carriers are translated into RTVI frames for the client.
- `UIWorker` auto-injects the UI wire-format guide (`UI_STATE_PROMPT_GUIDE`) into its LLM's system instruction by default, via a `prompt_guide` parameter — pass your own string to override the guide, or `None` to disable. Apps no longer need to concatenate `UI_STATE_PROMPT_GUIDE` into the LLM's `system_instruction` by hand.

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, MixerEnableFrame, MixerUpdateSettingsFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -105,7 +105,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -120,27 +120,27 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
logger.info(f"Listening for background sound for a bit...")
await asyncio.sleep(5.0)
logger.info(f"Reducing volume...")
await task.queue_frame(MixerUpdateSettingsFrame({"volume": 0.5}))
await worker.queue_frame(MixerUpdateSettingsFrame({"volume": 0.5}))
await asyncio.sleep(5.0)
logger.info(f"Disabling background sound for a bit...")
await task.queue_frame(MixerEnableFrame(False))
await worker.queue_frame(MixerEnableFrame(False))
await asyncio.sleep(5.0)
logger.info(f"Re-enabling background sound and starting bot...")
await task.queue_frame(MixerEnableFrame(True))
await worker.queue_frame(MixerEnableFrame(True))
# Kick off the conversation.
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -54,7 +54,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -146,7 +146,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -161,12 +161,12 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
# Start recording audio
await audiobuffer.start_recording()
# Start conversation - empty prompt to let LLM follow system instructions
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
# Handler for merged audio
@audiobuffer.event_handler("on_audio_data")
@@ -191,7 +191,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
await save_audio_file(bot_audio, bot_filename, sample_rate, 1)
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -20,7 +20,7 @@ from pipecat.frames.frames import (
)
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineTask
from pipecat.pipeline.worker import PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -144,7 +144,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
)
@@ -153,17 +153,17 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frame(TTSSpeakFrame("Hi, I'm listening!"))
await worker.queue_frame(TTSSpeakFrame("Hi, I'm listening!"))
await transport.send_audio(sounds["ding1.wav"])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -26,7 +26,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_context_summarizer import SummaryAppliedEvent
from pipecat.processors.aggregators.llm_response_universal import (
@@ -198,7 +198,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -214,16 +214,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -24,7 +24,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_context_summarizer import SummaryAppliedEvent
from pipecat.processors.aggregators.llm_response_universal import (
@@ -159,7 +159,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -175,16 +175,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -26,7 +26,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, LLMSummarizeContextFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -133,7 +133,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -149,16 +149,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -24,7 +24,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_context_summarizer import SummaryAppliedEvent
from pipecat.processors.aggregators.llm_response_universal import (
@@ -159,7 +159,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -175,16 +175,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -56,7 +56,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, LLMSetToolsFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import NOT_GIVEN, LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -163,7 +163,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(enable_metrics=True, enable_usage_metrics=True),
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
@@ -185,13 +185,13 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
"=== Phase 1: weather tool REMOVED. Keep asking about the weather "
"to exercise hallucination scenarios. ==="
)
await task.queue_frame(LLMSetToolsFrame(tools=NOT_GIVEN))
await worker.queue_frame(LLMSetToolsFrame(tools=NOT_GIVEN))
elif user_turn_count == READD_AT_TURN - 1:
logger.info(
"=== Phase 2: weather tool RE-ADDED. Ask for the weather again — "
"does the LLM call it, or keep refusing? (THIS IS THE TEST.) ==="
)
await task.queue_frame(LLMSetToolsFrame(tools=weather_tools))
await worker.queue_frame(LLMSetToolsFrame(tools=weather_tools))
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
@@ -209,15 +209,15 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
),
}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -4,27 +4,27 @@
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Example demonstrating ``PipelineTask(app_resources=...)``.
"""Example demonstrating ``PipelineWorker(app_resources=...)``.
``app_resources`` is an application-defined bag of anything your
application code may want to share across a session: database handles,
HTTP clients, feature flags, per-user state, observability clients,
in-memory caches — whatever fits your app. Pipecat passes it through
untouched and exposes it as ``task.app_resources``, so any code with a
handle on the task can read or mutate it.
untouched and exposes it as ``worker.app_resources``, so any code with a
handle on the worker can read or mutate it.
Two of the convenience aliases exercised below:
- Tool handlers read it from ``FunctionCallParams.app_resources``.
- Custom ``FrameProcessor`` subclasses read it from
``self.pipeline_task.app_resources``.
``self.pipeline_worker.app_resources``.
This example uses two small loggers as stand-ins for that "shared thing":
``ToolCallLogger`` (written from tool handlers) and
``TranscriptionLogger`` (written from a custom ``FrameProcessor`` that
sits in the pipeline). A real app might just as easily pass a Postgres
pool, a Redis client, a Stripe SDK instance, or any combination thereof.
The mechanics shown here — construct once, hand to the task, read it
The mechanics shown here — construct once, hand to the worker, read it
from each site, inspect it after the session — are the same regardless
of what you put in.
@@ -50,7 +50,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import Frame, LLMRunFrame, TranscriptionFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -131,7 +131,7 @@ class AppResources:
get autocomplete and refactor safety:
- In tools: ``cast(AppResources, params.app_resources)``.
- In custom processors: ``cast(AppResources, self.pipeline_task.app_resources)``.
- In custom processors: ``cast(AppResources, self.pipeline_worker.app_resources)``.
"""
tool_call_logger: ToolCallLogger
@@ -155,8 +155,8 @@ class TranscriptionLoggingProcessor(FrameProcessor):
Demonstrates the second read site for ``app_resources``: any custom
``FrameProcessor`` can reach the same bag every tool handler sees by
going through ``self.pipeline_task.app_resources``. ``pipeline_task``
is ``None`` until the task sets the processor up, so we guard against
going through ``self.pipeline_worker.app_resources``. ``pipeline_worker``
is ``None`` until the worker sets the processor up, so we guard against
that case.
"""
@@ -164,8 +164,8 @@ class TranscriptionLoggingProcessor(FrameProcessor):
"""Forward all frames; log final user transcriptions on the way through."""
await super().process_frame(frame, direction)
if isinstance(frame, TranscriptionFrame) and self.pipeline_task is not None:
resources = cast(AppResources, self.pipeline_task.app_resources)
if isinstance(frame, TranscriptionFrame) and self.pipeline_worker is not None:
resources = cast(AppResources, self.pipeline_worker.app_resources)
resources.transcription_logger.log_transcription(frame.text)
await self.push_frame(frame, direction)
@@ -282,7 +282,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
transcription_logger=transcription_logger,
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -299,16 +299,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
# The session has ended; read whatever state the handlers built up.
logger.info(f"Tool calls logged during session:\n{tool_call_logger.dump()}")

View File

@@ -14,7 +14,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import DataFrame, LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -97,7 +97,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -124,16 +124,18 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
{"role": "developer", "content": "Please introduce yourself to the user."}
)
# Custom frames are pushed in order so they can be used for synchronization purposes.
await task.queue_frames([CustomBeforeProcessFrame(), LLMRunFrame(), CustomAfterPushFrame()])
await worker.queue_frames(
[CustomBeforeProcessFrame(), LLMRunFrame(), CustomAfterPushFrame()]
)
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.parallel_pipeline import ParallelPipeline
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -130,7 +130,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -149,16 +149,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
groq_context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -21,7 +21,7 @@ from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.parallel_pipeline import ParallelPipeline
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -141,7 +141,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -160,16 +160,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
evaluator_context.add_message(
{"role": "developer", "content": "Ready to evaluate user messages."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -17,7 +17,7 @@ from pipecat.frames.frames import (
)
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -128,7 +128,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -144,16 +144,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -14,7 +14,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -95,7 +95,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -112,7 +112,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
# Handle "latency-ping" messages. The client will send app messages that look like
# this:
@@ -128,13 +128,13 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
logger.debug(f"Received latency ping app message: {message}")
ts = message["latency-ping"]["ts"]
# Send immediately
await task.queue_frame(
await worker.queue_frame(
DailyOutputTransportMessageUrgentFrame(
message={"latency-pong-msg-handler": {"ts": ts}}, participant_id=sender
)
)
# And push to the pipeline for the Daily transport.output to send
await task.queue_frame(
await worker.queue_frame(
DailyOutputTransportMessageFrame(
message={"latency-pong-pipeline-delivery": {"ts": ts}},
participant_id=sender,
@@ -146,11 +146,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -14,7 +14,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -99,7 +99,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -111,7 +111,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
logger.info(f"Client connected")
await task.queue_frames(
await worker.queue_frames(
[
TTSSpeakFrame(
text="Hello, welcome to live translation. Everything you say will be automatically translated to Spanish. Let's begin!",
@@ -123,11 +123,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -48,7 +48,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSUpdateSettingsFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -216,7 +216,7 @@ Remember: Use narrator voice for EVERYTHING except the actual quoted dialogue.""
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -229,15 +229,15 @@ Remember: Use narrator voice for EVERYTHING except the actual quoted dialogue.""
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Start conversation - empty prompt to let LLM follow system instructions
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -18,7 +18,7 @@ from pipecat.pipeline.llm_switcher import LLMSwitcher
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.service_switcher import ServiceSwitcher
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -151,7 +151,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -167,25 +167,25 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
await asyncio.sleep(15)
print(f"Switching to {stt_deepgram}")
await task.queue_frames([ManuallySwitchServiceFrame(service=stt_deepgram)])
await worker.queue_frames([ManuallySwitchServiceFrame(service=stt_deepgram)])
await asyncio.sleep(15)
print(f"Switching to {llm_google}")
await task.queue_frames([ManuallySwitchServiceFrame(service=llm_google)])
await worker.queue_frames([ManuallySwitchServiceFrame(service=llm_google)])
await asyncio.sleep(15)
print(f"Switching to {tts_deepgram}")
await task.queue_frames([ManuallySwitchServiceFrame(service=tts_deepgram)])
await worker.queue_frames([ManuallySwitchServiceFrame(service=tts_deepgram)])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -17,7 +17,7 @@ from pipecat.frames.frames import Frame, LLMRunFrame
from pipecat.pipeline.parallel_pipeline import ParallelPipeline
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -147,7 +147,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -166,16 +166,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
"content": f"Please introduce yourself to the user and let them know the languages you speak. Your initial responses should be in {tts.current_language}.",
}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -17,7 +17,7 @@ from pipecat.frames.frames import Frame, LLMRunFrame
from pipecat.pipeline.parallel_pipeline import ParallelPipeline
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -157,7 +157,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -176,16 +176,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
"content": f"Please introduce yourself to the user and let them know the voices you can do. Your initial responses should be as if you were a {tts.current_voice}.",
}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -125,7 +125,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -138,15 +138,15 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Start conversation - empty prompt to let LLM follow system instructions
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -14,7 +14,7 @@ from pipecat.extensions.voicemail.voicemail_detector import VoicemailDetector
from pipecat.frames.frames import TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -91,7 +91,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -107,7 +107,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
@voicemail.event_handler("on_conversation_detected")
async def on_conversation_detected(processor):
@@ -130,7 +130,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -13,7 +13,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -107,7 +107,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -123,16 +123,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -38,7 +38,7 @@ from pipecat.frames.frames import (
)
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -171,7 +171,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -186,16 +186,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -140,7 +140,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -156,16 +156,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame, UserImageRequestFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -143,7 +143,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -168,16 +168,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
"content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.",
}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -125,7 +125,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -141,16 +141,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame, UserImageRequestFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -148,7 +148,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -173,16 +173,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
"content": f"Please introduce yourself to the user briefly; don't mention the camera. Use '{client_id}' as the user ID during function calls.",
}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -14,7 +14,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -134,7 +134,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -150,16 +150,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -120,7 +120,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -133,16 +133,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -129,7 +129,7 @@ Start by asking me for my location. Then, use 'get_weather_current' to give me a
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -142,16 +142,16 @@ Start by asking me for my location. Then, use 'get_weather_current' to give me a
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -130,7 +130,7 @@ Start by asking me for my location. Then, use 'get_weather_current' to give me a
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -143,16 +143,16 @@ Start by asking me for my location. Then, use 'get_weather_current' to give me a
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -121,7 +121,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -134,16 +134,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -123,7 +123,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -139,16 +139,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -38,7 +38,7 @@ from pipecat.frames.frames import (
)
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -175,7 +175,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -190,16 +190,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame, UserImageRequestFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -208,7 +208,7 @@ indicate you should use the get_image tool are:
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -232,16 +232,16 @@ indicate you should use the get_image tool are:
"content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.",
}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -121,7 +121,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -140,16 +140,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
"content": "Please introduce yourself to the user.",
}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame, UserImageRequestFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -143,7 +143,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -168,16 +168,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
"content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.",
}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame, UserImageRequestFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -200,7 +200,7 @@ indicate you should use the get_image tool are:
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -224,16 +224,16 @@ indicate you should use the get_image tool are:
"content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.",
}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -17,7 +17,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -117,7 +117,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -133,16 +133,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -118,7 +118,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -131,16 +131,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -48,7 +48,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -141,7 +141,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(enable_metrics=True, enable_usage_metrics=True),
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
@@ -164,15 +164,15 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
),
}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -131,7 +131,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -144,16 +144,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -24,7 +24,7 @@ from pipecat.frames.frames import (
from pipecat.pipeline.parallel_pipeline import ParallelPipeline
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineTask
from pipecat.pipeline.worker import PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -185,7 +185,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
)
@@ -206,16 +206,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
"content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.",
}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -135,7 +135,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -151,16 +151,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -136,7 +136,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -149,16 +149,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -122,7 +122,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -135,16 +135,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -136,7 +136,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -149,16 +149,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -38,7 +38,7 @@ from pipecat.frames.frames import (
)
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -175,7 +175,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -190,16 +190,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -19,7 +19,7 @@ from pipecat.frames.frames import (
)
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -153,7 +153,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -168,16 +168,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -38,7 +38,7 @@ from pipecat.frames.frames import (
)
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -175,7 +175,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -187,16 +187,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -157,7 +157,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -173,16 +173,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -135,7 +135,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -151,16 +151,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame, UserImageRequestFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -143,7 +143,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -167,12 +167,12 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
"content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.",
}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
@tts.event_handler("on_tts_request")
async def on_tts_request(tts, context_id: str, text: str):
@@ -180,7 +180,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame, UserImageRequestFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -143,7 +143,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -167,12 +167,12 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
"content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.",
}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
@tts.event_handler("on_tts_request")
async def on_tts_request(tts, context_id: str, text: str):
@@ -180,7 +180,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -143,7 +143,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -159,16 +159,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame, UserImageRequestFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -143,7 +143,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -167,12 +167,12 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
"content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.",
}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
@tts.event_handler("on_tts_request")
async def on_tts_request(tts, context_id: str, text: str):
@@ -180,7 +180,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -136,7 +136,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -148,16 +148,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -123,7 +123,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -136,16 +136,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -20,7 +20,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -92,7 +92,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -108,16 +108,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -121,7 +121,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -134,16 +134,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -120,7 +120,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -133,16 +133,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -140,7 +140,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -156,16 +156,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -118,7 +118,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -134,16 +134,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -12,7 +12,7 @@ from loguru import logger
from pipecat.frames.frames import EndFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineTask
from pipecat.pipeline.worker import PipelineWorker
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
from pipecat.services.cartesia.tts import CartesiaTTSService
@@ -42,7 +42,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
),
)
task = PipelineTask(
worker = PipelineWorker(
Pipeline([tts, transport.output()]),
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
)
@@ -50,11 +50,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
# Register an event handler so we can play the audio when the client joins
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
await task.queue_frames([TTSSpeakFrame(f"Hello there!"), EndFrame()])
await worker.queue_frames([TTSSpeakFrame(f"Hello there!"), EndFrame()])
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -14,7 +14,7 @@ from loguru import logger
from pipecat.frames.frames import EndFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineTask
from pipecat.pipeline.worker import PipelineWorker
from pipecat.services.cartesia.tts import CartesiaTTSService
from pipecat.transports.local.audio import LocalAudioTransport, LocalAudioTransportParams
@@ -36,15 +36,15 @@ async def main():
pipeline = Pipeline([tts, transport.output()])
task = PipelineTask(pipeline)
worker = PipelineWorker(pipeline)
async def say_something():
await asyncio.sleep(1)
await task.queue_frames([TTSSpeakFrame("Hello there, how is it going!"), EndFrame()])
await worker.queue_frames([TTSSpeakFrame("Hello there, how is it going!"), EndFrame()])
runner = PipelineRunner(handle_sigint=False if sys.platform == "win32" else True)
await asyncio.gather(runner.run(task), say_something())
await asyncio.gather(runner.run(worker), say_something())
if __name__ == "__main__":

View File

@@ -12,7 +12,7 @@ from loguru import logger
from pipecat.frames.frames import EndFrame, LLMContextFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineTask
from pipecat.pipeline.worker import PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
@@ -51,7 +51,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
),
)
task = PipelineTask(
worker = PipelineWorker(
Pipeline([llm, tts, transport.output()]),
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
)
@@ -61,11 +61,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
context = LLMContext()
context.add_message({"role": "developer", "content": "Say hello to the world."})
await task.queue_frames([LLMContextFrame(context), EndFrame()])
await worker.queue_frames([LLMContextFrame(context), EndFrame()])
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -12,7 +12,7 @@ from loguru import logger
from pipecat.frames.frames import TextFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
from pipecat.services.google.image import GoogleImageGenService
@@ -45,7 +45,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
api_key=os.environ["GOOGLE_API_KEY"],
)
task = PipelineTask(
worker = PipelineWorker(
Pipeline([imagegen, transport.output()]),
params=PipelineParams(
enable_metrics=True,
@@ -57,18 +57,18 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
# Register an event handler so we can play the audio when the client joins
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
await task.queue_frame(TextFrame("a cat in the style of picasso"))
await task.queue_frame(TextFrame("a dog in the style of picasso"))
await task.queue_frame(TextFrame("a fish in the style of picasso"))
await worker.queue_frame(TextFrame("a cat in the style of picasso"))
await worker.queue_frame(TextFrame("a dog in the style of picasso"))
await worker.queue_frame(TextFrame("a fish in the style of picasso"))
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -16,7 +16,7 @@ from loguru import logger
from pipecat.frames.frames import TextFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineTask
from pipecat.pipeline.worker import PipelineWorker
from pipecat.services.fal.image import FalImageGenService
from pipecat.transports.local.tk import TkLocalTransport, TkTransportParams
@@ -46,18 +46,18 @@ async def main():
pipeline = Pipeline([imagegen, transport.output()])
task = PipelineTask(pipeline)
await task.queue_frames([TextFrame("a cat in the style of picasso")])
worker = PipelineWorker(pipeline)
await worker.queue_frames([TextFrame("a cat in the style of picasso")])
runner = PipelineRunner()
async def run_tk():
while not task.has_finished():
while not worker.has_finished():
tk_root.update()
tk_root.update_idletasks()
await asyncio.sleep(0.1)
await asyncio.gather(runner.run(task), run_tk())
await asyncio.gather(runner.run(worker), run_tk())
if __name__ == "__main__":

View File

@@ -22,7 +22,7 @@ from pipecat.frames.frames import (
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.sync_parallel_pipeline import FrameOrder, SyncParallelPipeline
from pipecat.pipeline.task import PipelineTask
from pipecat.pipeline.worker import PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.sentence import SentenceAggregator
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
@@ -186,7 +186,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
frames.append(MonthFrame(month=month))
frames.append(LLMContextFrame(LLMContext(messages)))
task = PipelineTask(
worker = PipelineWorker(
pipeline,
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
)
@@ -196,16 +196,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Start the month narration once connected
await task.queue_frames(frames)
await worker.queue_frames(frames)
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
# Run the pipeline
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -20,7 +20,7 @@ from pipecat.frames.frames import (
)
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -136,7 +136,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -149,15 +149,15 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -13,7 +13,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -85,7 +85,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -101,16 +101,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -74,7 +74,7 @@ async def main():
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -83,11 +83,11 @@ async def main():
)
context.add_message({"role": "developer", "content": "Please introduce yourself to the user."})
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
runner = PipelineRunner()
await runner.run(task)
await runner.run(worker)
if __name__ == "__main__":

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -135,7 +135,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -151,16 +151,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
context.add_message(
{"role": "developer", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -18,7 +18,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -130,7 +130,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -143,16 +143,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected: {client}")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -108,7 +108,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -121,16 +121,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected: {client}")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -97,7 +97,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -110,16 +110,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
async def on_client_connected(transport, client):
logger.info(f"Client connected: {client}")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -15,7 +15,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
@@ -112,7 +112,7 @@ Just respond with short sentences when you are carrying out tool calls.
]
)
task = PipelineTask(
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
@@ -125,16 +125,16 @@ Just respond with short sentences when you are carrying out tool calls.
async def on_client_connected(transport, client):
logger.info(f"Client connected: {client}")
# Kick off the conversation.
await task.queue_frames([LLMRunFrame()])
await worker.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
await worker.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
await runner.run(worker)
async def bot(runner_args: RunnerArguments):

View File

@@ -0,0 +1,369 @@
# Pipecat Multi-Worker Examples
This directory contains example bots that use the multi-worker framework in `pipecat.workers`, `pipecat.pipeline.runner` (with `add_workers()`), and the `WorkerBus`. Each example shows a different cooperation pattern between workers: hand-off, parallel fan-out, remote workers, etc.
## Setup
From the repo root:
```bash
uv sync --all-extras
source .venv/bin/activate
cd examples/multi-worker
```
Copy the env template and fill in your API keys:
```bash
cp env.example .env
```
## Environment variables
| Variable | Required by |
| ------------------ | --------------------------------------- |
| `OPENAI_API_KEY` | LLM workers |
| `DEEPGRAM_API_KEY` | STT |
| `CARTESIA_API_KEY` | TTS |
| `DAILY_API_KEY` | Optional: only with `--transport daily` |
Additional, example-specific variables are listed below.
## Table of contents
**[Local](#local)** (single process)
- [Handoff between LLM workers](#handoff-between-llm-tasks)
- [Parallel debate](#parallel-debate)
- [Voice code assistant with Claude Agent SDK](#voice-code-assistant)
- [Sensor controller](#sensor-controller)
**[Distributed](#distributed)** (multi-process)
- [Handoff via Redis](#handoff-via-redis)
- [Handoff via PGMQ (Postgres)](#handoff-via-pgmq-postgres)
- [LLM worker via WebSocket proxy](#llm-task-via-websocket-proxy)
# Local
Examples where all workers run in the same process on an `AsyncQueueBus`.
## Handoff between LLM workers
Two LLM workers (greeter + support) that transfer control to each other during a voice conversation. A main worker owns the transport pipeline and bridges frames to the bus.
### Running
```bash
uv run local-handoff/local-handoff-two-agents.py
```
Open <http://localhost:7860/client> in your browser to talk to your bot.
To use Daily transport:
```bash
uv run local-handoff/local-handoff-two-agents.py --transport daily
```
### Overview
- **[`local-handoff-two-agents.py`](local-handoff/local-handoff-two-agents.py)** — Two LLM workers (greeter + support) that hand off via `activate_worker(..., deactivate_self=True)`. The main worker owns STT, TTS, transport, and a `BusBridgeProcessor`.
- **[`local-handoff-two-agents-tts.py`](local-handoff/local-handoff-two-agents-tts.py)** — Same shape, but each child worker ships with its own `CartesiaTTSService` in a custom pipeline. The main worker has no TTS — audio comes from whichever child is active over the bus.
## Parallel debate
Parallel fan-out using `worker.job_group(...)`. A voice bot takes a topic from the user, kicks off three workers in parallel (advocate, critic, analyst), waits for all three to respond, and synthesizes a balanced answer. Each worker keeps its own LLM context across rounds.
### Running
```bash
uv run parallel-debate/parallel-debate.py
```
Open <http://localhost:7860/client> in your browser to talk to your bot.
To use Daily transport:
```bash
uv run parallel-debate/parallel-debate.py --transport daily
```
### Architecture
```
Main worker (transport + LLM + `debate` tool)
└── job_group(advocate, critic, analyst)
└── DebateWorker (LLMContextWorker, one per role)
```
- **Main worker**: transport (STT, TTS) + LLM moderator with a `debate` direct function that fans out via `worker.job_group(...)`.
- **Debate workers**: `LLMContextWorker`s spawned on the runner. Each keeps its own `LLMContext` across rounds and ships its completed turn back as a job response via the assistant-aggregator's `on_assistant_turn_stopped` event.
## Voice code assistant
Talk to your codebase hands-free. Ask questions about code, project structure, or file contents and get spoken answers based on actual files. The Claude Agent SDK worker navigates the filesystem using `Read`, `Bash`, `Glob`, and `Grep` tools.
### Additional environment variables
| Variable | Required by |
| ------------------- | ------------------------------ |
| `ANTHROPIC_API_KEY` | Code worker (Claude Agent SDK) |
| `PROJECT_PATH` | Optional, defaults to cwd |
### Running
```bash
# Default: explores the current directory
uv run code-assistant/code-assistant.py
# Specify a project path
PROJECT_PATH=/path/to/your/project uv run code-assistant/code-assistant.py
```
Open <http://localhost:7860/client> in your browser to talk to your bot.
To use Daily transport:
```bash
uv run code-assistant/code-assistant.py --transport daily
```
### Example questions
- "What does the main module do?"
- "Find all TODO comments in the project"
- "How is error handling implemented?"
- "What dependencies does this project use?"
- "Explain the test structure"
### Architecture
```
Main worker (transport + LLM + `ask_code` tool)
└── job → CodeWorker (Claude Agent SDK)
```
- **`code-assistant.py`** — Main worker: STT, LLM (with system prompt + `ask_code` direct function), TTS, and transport. The `ask_code` tool dispatches a job to the worker via `worker.job("code_worker", payload=...)`.
- **`code_worker.py`** — `CodeWorker`: a bus-only `BaseWorker` spawned on the runner. It accepts `@job`-style requests through the bus and runs them sequentially through a persistent Claude SDK session so follow-up questions share context.
## Sensor controller
Two `PipelineWorker`s side by side, communicating only over job RPC. A voice agent has a single `ask_controller(question)` tool that forwards every temperature-related request to a worker; the worker owns a simulated thermometer and its own tool-calling LLM that decides how to answer (read the current value, inspect rolling stats, change the target, change the response rate). The worker is a plain `PipelineWorker` — it does not subclass `LLMWorker` and is not bridged.
### Running
```bash
uv run sensor-controller/sensor-controller.py
```
Open <http://localhost:7860/client> in your browser to talk to your bot.
To use Daily transport:
```bash
uv run sensor-controller/sensor-controller.py --transport daily
```
### Example questions
- "What's the temperature?"
- "Make it warmer."
- "Is it stable yet?"
- "Why is it slow?" / "Speed up the response."
- "What was the highest reading?"
### Architecture
```
Voice agent (transport + STT + LLM + TTS, tool: ask_controller)
└── job → Controller (PipelineWorker)
└── SensorReader -> SensorStats -> user_agg -> llm -> assistant_agg
```
- **[`sensor-controller.py`](sensor-controller/sensor-controller.py)** — `build_sensor_controller()` returns a plain `PipelineWorker`. Jobs arrive via `@worker.event_handler("on_job_request")`, the question is queued onto the worker LLM, and the LLM's reply is paired back to the job via the assistant aggregator's `on_assistant_turn_stopped` event.
- **[`sensor.py`](sensor-controller/sensor.py)** — Two custom `FrameProcessor` subclasses: `SensorReader` runs an autonomous tick loop that emits a `SensorReadingFrame` each second (first-order lag toward target plus Gaussian noise; mutable target and response rate); `SensorStats` maintains rolling min/max/avg/trend.
# Distributed
Examples where workers run across separate processes or machines.
## Handoff via Redis
Same two-worker handoff as the local example, but each worker runs as a separate process connected via Redis pub/sub. Requires `pip install pipecat-ai[redis]`.
### Quick start (single machine, local Redis)
_Terminal 1_: start Redis
```bash
docker run --rm -p 6379:6379 redis:7
```
_Terminal 2_: start the greeter worker
```bash
uv run distributed-handoff/redis-handoff/llm.py greeter
```
_Terminal 3_: start the support worker
```bash
uv run distributed-handoff/redis-handoff/llm.py support
```
_Terminal 4_: start the main transport worker
```bash
uv run distributed-handoff/redis-handoff/main.py
```
All processes connect to `redis://localhost:6379` by default.
### Running across machines
Point each process at the same Redis instance:
_Machine A_
```bash
uv run distributed-handoff/redis-handoff/main.py --redis-url redis://your-redis-host:6379
```
_Machine B_
```bash
uv run distributed-handoff/redis-handoff/llm.py greeter --redis-url redis://your-redis-host:6379
```
_Machine C_
```bash
uv run distributed-handoff/redis-handoff/llm.py support --redis-url redis://your-redis-host:6379
```
### Architecture
```
Machine A Redis Machine B
+------------+ +-------------+ +-------------+
| main.py | <----> | pub/sub | <----> | llm.py |
| (transport,| | channel: | | (greeter) |
| STT, TTS) | | pipecat:acme| +-------------+
+------------+ +-------------+ +-------------+
^ | llm.py |
+--------------> | (support) |
+-------------+
```
- **[main.py](distributed-handoff/redis-handoff/main.py)** — Transport worker: Daily/WebRTC, Deepgram STT, Cartesia TTS, and a `BusBridgeProcessor` over a `RedisBus`.
- **[llm.py](distributed-handoff/redis-handoff/llm.py)** — LLM worker: runs either `greeter` or `support` with OpenAI behind a bridged `LLMWorker`.
## Handoff via PGMQ (Postgres)
Same shape as the Redis handoff, but the bus is backed by [PGMQ](https://github.com/tembo-io/pgmq) on a shared Postgres database (e.g. Supabase). Requires `pip install pipecat-ai[pgmq]`.
### Additional environment variables
| Variable | Required by |
| -------------- | -------------------------------------------------------------------- |
| `DATABASE_URL` | PostgreSQL DSN (e.g. Supabase pooled connection string) |
| `PGMQ_CHANNEL` | Optional, channel prefix for queue names. Defaults to `pipecat_acme` |
### Quick start
_Terminal 1_: start the greeter worker
```bash
uv run distributed-handoff/pgmq-handoff/llm.py greeter --database-url $DATABASE_URL
```
_Terminal 2_: start the support worker
```bash
uv run distributed-handoff/pgmq-handoff/llm.py support --database-url $DATABASE_URL
```
_Terminal 3_: start the main transport worker
```bash
uv run distributed-handoff/pgmq-handoff/main.py --database-url $DATABASE_URL
```
You can also set `DATABASE_URL` in `.env` and omit the `--database-url` flag.
### Architecture
Same as the Redis handoff above; the `RedisBus` is replaced by a `PgmqBus`, and the "pub/sub channel" is a set of PGMQ queues on the shared Postgres instance.
## LLM worker via WebSocket proxy
Runs an LLM worker on a remote server, connected to the main transport worker via a WebSocket proxy. No shared bus required — the proxy workers forward bus messages point-to-point over the WebSocket.
### Quick start (single machine)
_Terminal 1_: start the remote assistant server
```bash
uv run remote-proxy-assistant/assistant.py
```
_Terminal 2_: start the main transport worker
```bash
uv run remote-proxy-assistant/main.py --remote-url ws://localhost:8765/ws
```
Open <http://localhost:7860/client> in your browser to talk to the bot.
### Running across machines
_Server machine_: start the assistant
```bash
uv run remote-proxy-assistant/assistant.py --host 0.0.0.0 --port 8765
```
_Client machine_: point at the server
```bash
uv run remote-proxy-assistant/main.py --remote-url ws://server-host:8765/ws
```
### Architecture
```
+-------------+ +-------------+ +-------------+ +-----------------+
| | | | | | | |
| Main worker | | Proxy worker | <~~~~~> | Proxy worker | | Assistant worker |
| | | (client) | | (server) | | |
+-------------+ +-------------+ +-------------+ +-----------------+
messages messages messages messages
│ │ │ │
══════════╧═════════════════╧════════ ════════╧════════════════════╧═══════════
Task Bus Task Bus
═════════════════════════════════════ ═════════════════════════════════════════
```
- **[main.py](remote-proxy-assistant/main.py)** — Transport worker with STT, TTS, and a `BusBridge`. Spawns a `WebSocketProxyClientTask` that connects to the remote server and forwards `BusFrameMessage`s.
- **[assistant.py](remote-proxy-assistant/assistant.py)** — FastAPI server. Each WebSocket connection spawns a `WebSocketProxyServerTask` plus a bridged `AcmeAssistant` LLM worker on a per-session `PipelineRunner`.
### Security
The proxy workers filter messages by worker name:
- Only messages targeted at the remote worker cross the WebSocket
- Only messages targeted at the local worker are accepted from the WebSocket
- Broadcast messages never cross the WebSocket
Pass HTTP headers for authentication:
```python
proxy = WebSocketProxyClientTask(
"proxy",
url="wss://server-host:8765/ws",
remote_worker_name="assistant",
local_worker_name="acme",
headers={"Authorization": "Bearer <token>"},
)
```

View File

@@ -0,0 +1,178 @@
#
# Copyright (c) 2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Voice code assistant powered by Claude Agent SDK.
Talk to your codebase hands-free. Ask questions like "what does the
auth middleware do?" or "find all TODO comments" and get spoken answers
based on actual file contents. The Claude Agent SDK worker navigates
the filesystem using Read, Bash, Glob, and Grep tools.
Architecture::
Main worker (transport + LLM + ``ask_code`` tool)
└── job → CodeWorker (Claude Agent SDK)
Requirements:
- ANTHROPIC_API_KEY
- OPENAI_API_KEY
- DEEPGRAM_API_KEY
- CARTESIA_API_KEY
- DAILY_API_KEY (for Daily transport)
"""
import os
from code_worker import CodeWorker
from dotenv import load_dotenv
from loguru import logger
from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMMessagesAppendFrame, LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
LLMUserAggregatorParams,
)
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
from pipecat.services.cartesia.tts import CartesiaTTSService
from pipecat.services.deepgram.stt import DeepgramSTTService
from pipecat.services.llm_service import FunctionCallParams
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams
load_dotenv(override=True)
PROJECT_PATH = os.getenv("PROJECT_PATH", os.getcwd())
transport_params = {
"daily": lambda: DailyParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
"webrtc": lambda: TransportParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
}
async def ask_code(params: FunctionCallParams, question: str):
"""Ask a question about the codebase. A Claude Code worker will
explore the project by reading files, searching code, and running
commands. It remembers previous questions for follow-ups.
Args:
question (str): The question about code, files, structure,
dependencies, or anything in the project.
"""
logger.info(f"Asking code worker: '{question}'")
async with params.pipeline_worker.job("code_worker", payload={"question": question}) as job:
await params.llm.queue_frame(
LLMMessagesAppendFrame(
messages=[{"role": "developer", "content": "Give me a moment."}],
run_llm=True,
)
)
# The LLM keeps talking while the worker runs.
await params.result_callback(job.response)
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
logger.info("Starting code assistant")
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
stt = DeepgramSTTService(api_key=os.environ["DEEPGRAM_API_KEY"])
tts = CartesiaTTSService(
api_key=os.environ["CARTESIA_API_KEY"],
settings=CartesiaTTSService.Settings(
voice="9626c31c-bec5-4cca-baa8-f8ba9e84c8bc", # Jacqueline
),
)
llm = OpenAILLMService(
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(
system_instruction=(
"You are a voice interface to a code assistant powered by Claude Code. "
"Behind you is a worker that can read files, search code with grep and "
"glob patterns, and run bash commands on the project. It maintains "
"context across questions, so follow-up questions work naturally.\n\n"
"When the user asks anything about code, project structure, files, "
"dependencies, tests, or wants to explore the codebase, call the "
"ask_code tool. When the worker result comes back, summarize it naturally "
"for speaking. Keep responses concise and conversational.\n"
),
),
)
llm.register_direct_function(ask_code, cancel_on_interruption=False, timeout_secs=60)
context = LLMContext(tools=ToolsSchema(standard_tools=[ask_code]))
aggregators = LLMContextAggregatorPair(
context,
user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()),
)
pipeline = Pipeline(
[
transport.input(),
stt,
aggregators.user(),
llm,
tts,
transport.output(),
aggregators.assistant(),
]
)
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
enable_usage_metrics=True,
),
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
)
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
logger.info("Client connected")
context.add_message(
{
"role": "developer",
"content": "Greet the user and tell them you're a code assistant.",
}
)
await worker.queue_frame(LLMRunFrame())
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await runner.cancel()
await runner.add_workers(CodeWorker("code_worker", project_path=PROJECT_PATH), worker)
await runner.run()
async def bot(runner_args: RunnerArguments):
"""Main bot entry point compatible with Pipecat Cloud."""
transport = await create_transport(runner_args, transport_params)
await run_bot(transport, runner_args)
if __name__ == "__main__":
from pipecat.runner.run import main
main()

View File

@@ -0,0 +1,118 @@
#
# Copyright (c) 2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Code worker that explores a codebase using Claude Agent SDK."""
import asyncio
from loguru import logger
from pipecat.bus import BusJobRequestMessage
from pipecat.pipeline.base_worker import BaseWorker
from pipecat.pipeline.job_context import JobStatus
try:
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
except ModuleNotFoundError as e:
logger.error(f"Exception: {e}")
logger.error("In order to use CodeWorker, you need to `pip install claude-agent-sdk`.")
raise Exception(f"Missing module: {e}")
class CodeWorker(BaseWorker):
"""Bus-only worker that answers code questions using Claude Agent SDK.
Maintains a persistent Claude SDK session so follow-up questions
share context. Questions are queued and processed sequentially. The
worker has no Pipecat pipeline — it consumes job requests from the
bus and replies with job responses.
"""
def __init__(self, name: str, *, project_path: str):
"""Initialize the CodeWorker.
Args:
name: Unique worker name.
project_path: Filesystem path the Claude SDK should explore.
"""
super().__init__(name)
self._project_path = project_path
self._queue: asyncio.Queue = asyncio.Queue()
self._worker_task: asyncio.Task | None = None
self._claude_options = ClaudeAgentOptions(
permission_mode="bypassPermissions",
system_prompt=(
f"You are a code assistant. The project is at: {self._project_path}\n\n"
"Answer the user's question by exploring the codebase. Use Read to "
"view files, Glob to find files by pattern, and Bash to run commands "
"like grep or find. Be thorough but concise in your answer. "
"Focus on what the user asked. Respond with a clear, spoken-friendly "
"summary (no markdown, no bullet points, no code blocks)."
),
allowed_tools=["Read", "Bash", "Glob", "Grep"],
model="sonnet",
max_turns=10,
)
async def start(self) -> None:
"""Launch the Claude SDK worker loop alongside the standard worker start."""
await super().start()
self._worker_task = self.create_task(self._worker_loop(), "worker")
async def stop(self) -> None:
"""Cancel the worker loop before tearing down the worker."""
if self._worker_task:
await self.cancel_task(self._worker_task)
self._worker_task = None
await super().stop()
async def on_job_request(self, message: BusJobRequestMessage) -> None:
"""Enqueue an incoming job for the worker loop."""
await super().on_job_request(message)
logger.info(f"Worker '{self.name}': queued '{message.payload['question']}'")
self._queue.put_nowait(message)
async def _worker_loop(self):
client = ClaudeSDKClient(options=self._claude_options)
try:
await client.connect()
except Exception as e:
logger.error(f"Worker '{self.name}': failed to start Claude SDK: {e}")
return
try:
while True:
message = await self._queue.get()
question = message.payload["question"]
logger.info(f"Worker '{self.name}': researching '{question}'")
try:
answer = ""
await client.query(prompt=question)
async for msg in client.receive_response():
if type(msg).__name__ == "AssistantMessage":
for block in msg.content:
if type(block).__name__ == "TextBlock":
answer += block.text
logger.info(f"Worker '{self.name}': completed ({len(answer)} chars)")
await self.send_job_response(message.job_id, {"answer": answer})
except Exception as e:
logger.error(f"Worker '{self.name}': error: {e}")
await self.send_job_response(
message.job_id, {"error": str(e)}, status=JobStatus.ERROR
)
finally:
# Bypass `async with ClaudeSDKClient` and call disconnect()
# ourselves: __aexit__ → Query.close() → _read_task.wait() uses
# `with suppress(asyncio.CancelledError)`, which would swallow the
# outer task's cancellation. By the time this finally runs, our
# CancelledError has already been raised once, so _must_cancel is
# cleared and disconnect()'s awaits proceed normally.
await client.disconnect()

View File

@@ -0,0 +1,182 @@
#
# Copyright (c) 2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""LLM worker — run on Machine B (or locally alongside ``main.py``).
A standalone process that runs one LLM worker (greeter or support)
attached to the same PGMQ-backed `WorkerBus` as the main worker.
Multiple instances can run on different machines as long as they
share a Postgres database with the PGMQ extension enabled.
Usage::
python llm.py greeter --database-url postgresql://...
python llm.py support --database-url postgresql://...
Requirements:
- OPENAI_API_KEY
- DATABASE_URL (or ``--database-url``)
"""
import argparse
import asyncio
import os
from urllib.parse import unquote, urlparse
from dotenv import load_dotenv
from loguru import logger
from pgmq.async_queue import PGMQueue
from pipecat.bus.network.pgmq import PgmqBus
from pipecat.pipeline.runner import PipelineRunner
from pipecat.services.llm_service import FunctionCallParams
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.workers.llm import LLMWorker, LLMWorkerActivationArgs, tool
load_dotenv(override=True)
WORKER_CONFIG = {
"greeter": {
"system_instruction": (
"You are a friendly greeter for Acme Corp. The available products "
"are: the Acme Rocket Boots, the Acme Invisible Paint, and the Acme "
"Tornado Kit. Ask which one they'd like to learn more about. "
"When the user picks a product or asks a question about one, "
"immediately call the transfer_to_agent tool with agent 'support'. "
"Do not answer product questions yourself. If the user says goodbye, "
"call the end_conversation tool. Do not mention transferring — just do it "
"seamlessly. Keep responses brief — this is a voice conversation."
),
"watch": ["support"],
},
"support": {
"system_instruction": (
"You are a support agent for Acme Corp. You know about three "
"products: Acme Rocket Boots (jet-powered boots, $299, run up "
"to 60 mph), Acme Invisible Paint (makes anything invisible for "
"24 hours, $49 per can), and Acme Tornado Kit (portable tornado "
"generator, $199, batteries included). Answer the user's questions "
"about these products. If the user wants to browse other products "
"or start over, call the transfer_to_agent tool with agent "
"'greeter'. If the user says goodbye, call the end_conversation "
"tool. Do not mention transferring — just do it seamlessly. "
"Keep responses brief — this is a voice conversation."
),
"watch": ["greeter"],
},
}
def pgmq_from_url(database_url: str, *, pool_size: int = 4) -> PGMQueue:
"""Build a `PGMQueue` from a Postgres DSN string."""
parsed = urlparse(database_url)
if parsed.scheme not in ("postgres", "postgresql"):
raise ValueError(f"Unsupported scheme '{parsed.scheme}' for database URL")
return PGMQueue(
host=parsed.hostname or "localhost",
port=str(parsed.port or 5432),
database=(parsed.path or "/postgres").lstrip("/") or "postgres",
username=unquote(parsed.username or "postgres"),
password=unquote(parsed.password or ""),
pool_size=pool_size,
)
class AcmeLLMTask(LLMWorker):
"""LLM worker for Acme Corp with transfer and end tools."""
def __init__(self, name: str, *, system_instruction: str, watch: list[str]):
"""Initialize the AcmeLLMTask.
Args:
name: Unique worker name (``"greeter"`` or ``"support"``).
system_instruction: System prompt for this LLM role.
watch: Sibling worker names this worker will watch via the
registry so it knows when they become available for
handoff.
"""
llm = OpenAILLMService(
name=f"{name}::OpenAILLMService",
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(system_instruction=system_instruction),
)
super().__init__(name, llm=llm, bridged=())
self._watch = watch
async def start(self) -> None:
"""Register watches for sibling workers once ready."""
await super().start()
for worker_name in self._watch:
await self.watch_worker(worker_name)
@tool(cancel_on_interruption=False)
async def transfer_to_agent(self, params: FunctionCallParams, agent: str, reason: str):
"""Transfer the user to another agent.
Args:
agent (str): The agent to transfer to (e.g. 'greeter', 'support').
reason (str): Why the user is being transferred.
"""
logger.info(f"Task '{self.name}': transferring to '{agent}' ({reason})")
await self.activate_worker(
agent,
args=LLMWorkerActivationArgs(messages=[{"role": "developer", "content": reason}]),
deactivate_self=True,
result_callback=params.result_callback,
)
@tool
async def end_conversation(self, params: FunctionCallParams, reason: str):
"""End the conversation when the user says goodbye.
Args:
reason (str): Why the conversation is ending.
"""
logger.info(f"Task '{self.name}': ending conversation ({reason})")
await self.end(
reason=reason,
messages=[{"role": "developer", "content": reason}],
result_callback=params.result_callback,
)
async def main_async() -> None:
parser = argparse.ArgumentParser(description="LLM worker (greeter or support)")
parser.add_argument("worker", choices=list(WORKER_CONFIG), help="Which worker to run")
parser.add_argument(
"--database-url",
default=os.getenv("DATABASE_URL"),
help="PostgreSQL DSN (or set DATABASE_URL env var)",
)
parser.add_argument(
"--channel",
default=os.getenv("PGMQ_CHANNEL", "pipecat_acme"),
help="PGMQ channel prefix",
)
args = parser.parse_args()
if not args.database_url:
parser.error("--database-url is required (or set DATABASE_URL env var)")
pgmq = pgmq_from_url(args.database_url)
await pgmq.init()
bus = PgmqBus(pgmq=pgmq, channel=args.channel)
config = WORKER_CONFIG[args.worker]
worker = AcmeLLMTask(
args.worker,
system_instruction=config["system_instruction"],
watch=config["watch"],
)
runner = PipelineRunner(bus=bus, handle_sigint=True)
logger.info(f"Starting {args.worker} worker, waiting for activation...")
await runner.run(worker)
if __name__ == "__main__":
asyncio.run(main_async())

View File

@@ -0,0 +1,196 @@
#
# Copyright (c) 2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Main transport worker — run on Machine A.
Handles audio I/O (STT, TTS) and bridges frames to the bus. The LLM
workers run as separate processes (possibly on different
machines) connected via PGMQ on a shared Postgres database
(e.g. Supabase).
Usage::
python main.py --database-url postgresql://...
Requirements:
- DEEPGRAM_API_KEY
- CARTESIA_API_KEY
- DATABASE_URL (or ``--database-url``)
"""
import argparse
import os
from urllib.parse import unquote, urlparse
from dotenv import load_dotenv
from loguru import logger
from pgmq.async_queue import PGMQueue
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.bus import BusBridgeProcessor
from pipecat.bus.network.pgmq import PgmqBus
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
LLMUserAggregatorParams,
)
from pipecat.registry.types import WorkerReadyData
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
from pipecat.services.cartesia.tts import CartesiaTTSService
from pipecat.services.deepgram.stt import DeepgramSTTService
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams
from pipecat.workers.llm import LLMWorkerActivationArgs
load_dotenv(override=True)
MAIN_NAME = "acme"
transport_params = {
"daily": lambda: DailyParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
"webrtc": lambda: TransportParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
}
def pgmq_from_url(database_url: str, *, pool_size: int = 4) -> PGMQueue:
"""Build a `PGMQueue` from a Postgres DSN string."""
parsed = urlparse(database_url)
if parsed.scheme not in ("postgres", "postgresql"):
raise ValueError(f"Unsupported scheme '{parsed.scheme}' for database URL")
return PGMQueue(
host=parsed.hostname or "localhost",
port=str(parsed.port or 5432),
database=(parsed.path or "/postgres").lstrip("/") or "postgres",
username=unquote(parsed.username or "postgres"),
password=unquote(parsed.password or ""),
pool_size=pool_size,
)
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
pgmq = pgmq_from_url(runner_args.cli_args.database_url)
await pgmq.init()
bus = PgmqBus(pgmq=pgmq, channel=runner_args.cli_args.channel)
runner = PipelineRunner(bus=bus, handle_sigint=runner_args.handle_sigint)
stt = DeepgramSTTService(api_key=os.environ["DEEPGRAM_API_KEY"])
tts = CartesiaTTSService(
api_key=os.environ["CARTESIA_API_KEY"],
settings=CartesiaTTSService.Settings(
voice="9626c31c-bec5-4cca-baa8-f8ba9e84c8bc", # Jacqueline
),
)
context = LLMContext()
aggregators = LLMContextAggregatorPair(
context,
user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()),
)
bridge = BusBridgeProcessor(
bus=runner.bus,
worker_name=MAIN_NAME,
name=f"{MAIN_NAME}::BusBridge",
)
pipeline = Pipeline(
[
transport.input(),
stt,
aggregators.user(),
bridge,
tts,
transport.output(),
aggregators.assistant(),
]
)
worker = PipelineWorker(
pipeline,
name=MAIN_NAME,
params=PipelineParams(
enable_metrics=True,
enable_usage_metrics=True,
),
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
)
# The remote LLM workers may take a moment to register on the bus.
# We only activate ``greeter`` once *both* the client is connected
# and the worker has been observed via the registry.
state = {"client_connected": False, "greeter_ready": False}
async def maybe_activate():
if not (state["client_connected"] and state["greeter_ready"]):
return
await worker.activate_worker(
"greeter",
args=LLMWorkerActivationArgs(
messages=[
{
"role": "developer",
"content": (
"Welcome the user to Acme Corp, mention the available "
"products and ask how you can help."
),
},
],
),
)
async def on_greeter_ready(_data: WorkerReadyData) -> None:
state["greeter_ready"] = True
await maybe_activate()
await runner.registry.watch("greeter", on_greeter_ready)
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
logger.info("Client connected")
state["client_connected"] = True
await maybe_activate()
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await worker.cancel()
await runner.run(worker)
async def bot(runner_args: RunnerArguments):
"""Main bot entry point compatible with Pipecat Cloud."""
transport = await create_transport(runner_args, transport_params)
await run_bot(transport, runner_args)
if __name__ == "__main__":
from pipecat.runner.run import main
parser = argparse.ArgumentParser(description="Main transport worker (PGMQ bus)")
parser.add_argument(
"--database-url",
default=os.getenv("DATABASE_URL"),
help="PostgreSQL DSN (or set DATABASE_URL env var)",
)
parser.add_argument(
"--channel",
default=os.getenv("PGMQ_CHANNEL", "pipecat_acme"),
help="PGMQ channel prefix",
)
main(parser)

View File

@@ -0,0 +1,152 @@
#
# Copyright (c) 2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""LLM worker — run on Machine B (or locally alongside ``main.py``).
A standalone process that runs one LLM worker (greeter or support)
attached to the same Redis-backed `WorkerBus` as the main worker.
Multiple instances can run on different machines.
Usage::
python llm.py greeter --redis-url redis://localhost:6379
python llm.py support --redis-url redis://localhost:6379
Requirements:
- OPENAI_API_KEY
"""
import argparse
import asyncio
import os
from dotenv import load_dotenv
from loguru import logger
from redis.asyncio import Redis
from pipecat.bus.network.redis import RedisBus
from pipecat.pipeline.runner import PipelineRunner
from pipecat.services.llm_service import FunctionCallParams
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.workers.llm import LLMWorker, LLMWorkerActivationArgs, tool
load_dotenv(override=True)
WORKER_CONFIG = {
"greeter": {
"system_instruction": (
"You are a friendly greeter for Acme Corp. The available products "
"are: the Acme Rocket Boots, the Acme Invisible Paint, and the Acme "
"Tornado Kit. Ask which one they'd like to learn more about. "
"When the user picks a product or asks a question about one, "
"immediately call the transfer_to_agent tool with agent 'support'. "
"Do not answer product questions yourself. If the user says goodbye, "
"call the end_conversation tool. Do not mention transferring — just do it "
"seamlessly. Keep responses brief — this is a voice conversation."
),
"watch": ["support"],
},
"support": {
"system_instruction": (
"You are a support agent for Acme Corp. You know about three "
"products: Acme Rocket Boots (jet-powered boots, $299, run up "
"to 60 mph), Acme Invisible Paint (makes anything invisible for "
"24 hours, $49 per can), and Acme Tornado Kit (portable tornado "
"generator, $199, batteries included). Answer the user's questions "
"about these products. If the user wants to browse other products "
"or start over, call the transfer_to_agent tool with agent "
"'greeter'. If the user says goodbye, call the end_conversation "
"tool. Do not mention transferring — just do it seamlessly. "
"Keep responses brief — this is a voice conversation."
),
"watch": ["greeter"],
},
}
class AcmeLLMTask(LLMWorker):
"""LLM worker for Acme Corp with transfer and end tools."""
def __init__(self, name: str, *, system_instruction: str, watch: list[str]):
"""Initialize the AcmeLLMTask.
Args:
name: Unique worker name (``"greeter"`` or ``"support"``).
system_instruction: System prompt for this LLM role.
watch: Sibling worker names this worker will watch via the
registry so it knows when they become available for
handoff.
"""
llm = OpenAILLMService(
name=f"{name}::OpenAILLMService",
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(system_instruction=system_instruction),
)
super().__init__(name, llm=llm, bridged=())
self._watch = watch
async def start(self) -> None:
"""Register watches for sibling workers once ready."""
await super().start()
for worker_name in self._watch:
await self.watch_worker(worker_name)
@tool(cancel_on_interruption=False)
async def transfer_to_agent(self, params: FunctionCallParams, agent: str, reason: str):
"""Transfer the user to another agent.
Args:
agent (str): The agent to transfer to (e.g. 'greeter', 'support').
reason (str): Why the user is being transferred.
"""
logger.info(f"Task '{self.name}': transferring to '{agent}' ({reason})")
await self.activate_worker(
agent,
args=LLMWorkerActivationArgs(messages=[{"role": "developer", "content": reason}]),
deactivate_self=True,
result_callback=params.result_callback,
)
@tool
async def end_conversation(self, params: FunctionCallParams, reason: str):
"""End the conversation when the user says goodbye.
Args:
reason (str): Why the conversation is ending.
"""
logger.info(f"Task '{self.name}': ending conversation ({reason})")
await self.end(
reason=reason,
messages=[{"role": "developer", "content": reason}],
result_callback=params.result_callback,
)
async def main_async() -> None:
parser = argparse.ArgumentParser(description="LLM worker (greeter or support)")
parser.add_argument("worker", choices=list(WORKER_CONFIG), help="Which worker to run")
parser.add_argument("--redis-url", default="redis://localhost:6379", help="Redis URL")
parser.add_argument("--channel", default="pipecat:acme", help="Redis pub/sub channel")
args = parser.parse_args()
redis = Redis.from_url(args.redis_url)
bus = RedisBus(redis=redis, channel=args.channel)
config = WORKER_CONFIG[args.worker]
worker = AcmeLLMTask(
args.worker,
system_instruction=config["system_instruction"],
watch=config["watch"],
)
runner = PipelineRunner(bus=bus, handle_sigint=True)
logger.info(f"Starting {args.worker} worker, waiting for activation...")
await runner.run(worker)
if __name__ == "__main__":
asyncio.run(main_async())

View File

@@ -0,0 +1,169 @@
#
# Copyright (c) 2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Main transport worker — run on Machine A.
Handles audio I/O (STT, TTS) and bridges frames to the bus. The LLM
workers run as separate processes (possibly on different
machines) and connect to the same Redis-backed `WorkerBus`.
Usage::
python main.py --redis-url redis://localhost:6379
Requirements:
- DEEPGRAM_API_KEY
- CARTESIA_API_KEY
"""
import argparse
import os
from dotenv import load_dotenv
from loguru import logger
from redis.asyncio import Redis
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.bus import BusBridgeProcessor
from pipecat.bus.network.redis import RedisBus
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
LLMUserAggregatorParams,
)
from pipecat.registry.types import WorkerReadyData
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
from pipecat.services.cartesia.tts import CartesiaTTSService
from pipecat.services.deepgram.stt import DeepgramSTTService
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams
from pipecat.workers.llm import LLMWorkerActivationArgs
load_dotenv(override=True)
MAIN_NAME = "acme"
transport_params = {
"daily": lambda: DailyParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
"webrtc": lambda: TransportParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
}
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
redis = Redis.from_url(runner_args.cli_args.redis_url)
bus = RedisBus(redis=redis, channel=runner_args.cli_args.channel)
runner = PipelineRunner(bus=bus, handle_sigint=runner_args.handle_sigint)
stt = DeepgramSTTService(api_key=os.environ["DEEPGRAM_API_KEY"])
tts = CartesiaTTSService(
api_key=os.environ["CARTESIA_API_KEY"],
settings=CartesiaTTSService.Settings(
voice="9626c31c-bec5-4cca-baa8-f8ba9e84c8bc", # Jacqueline
),
)
context = LLMContext()
aggregators = LLMContextAggregatorPair(
context,
user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()),
)
bridge = BusBridgeProcessor(
bus=runner.bus,
worker_name=MAIN_NAME,
name=f"{MAIN_NAME}::BusBridge",
)
pipeline = Pipeline(
[
transport.input(),
stt,
aggregators.user(),
bridge,
tts,
transport.output(),
aggregators.assistant(),
]
)
worker = PipelineWorker(
pipeline,
name=MAIN_NAME,
params=PipelineParams(
enable_metrics=True,
enable_usage_metrics=True,
),
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
)
# The remote LLM workers may take a moment to register on the bus.
# We only activate ``greeter`` once *both* the client is connected
# and the worker has been observed via the registry.
state = {"client_connected": False, "greeter_ready": False}
async def maybe_activate():
if not (state["client_connected"] and state["greeter_ready"]):
return
await worker.activate_worker(
"greeter",
args=LLMWorkerActivationArgs(
messages=[
{
"role": "developer",
"content": (
"Welcome the user to Acme Corp, mention the available "
"products and ask how you can help."
),
},
],
),
)
async def on_greeter_ready(_data: WorkerReadyData) -> None:
state["greeter_ready"] = True
await maybe_activate()
await runner.registry.watch("greeter", on_greeter_ready)
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
logger.info("Client connected")
state["client_connected"] = True
await maybe_activate()
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await worker.cancel()
await runner.run(worker)
async def bot(runner_args: RunnerArguments):
"""Main bot entry point compatible with Pipecat Cloud."""
transport = await create_transport(runner_args, transport_params)
await run_bot(transport, runner_args)
if __name__ == "__main__":
from pipecat.runner.run import main
parser = argparse.ArgumentParser(description="Main transport worker (Redis bus)")
parser.add_argument("--redis-url", default="redis://localhost:6379", help="Redis URL")
parser.add_argument("--channel", default="pipecat:acme", help="Redis pub/sub channel")
main(parser)

View File

@@ -0,0 +1,22 @@
# Audio services
DEEPGRAM_API_KEY=
CARTESIA_API_KEY=
# LLM
OPENAI_API_KEY=
# Voice code assistant (Claude Agent SDK)
ANTHROPIC_API_KEY=
# Transport (optional — only needed with --transport daily)
DAILY_API_KEY=
# Distributed handoff via PGMQ
# Get from: Supabase dashboard > Project Settings > Database > Connection string
# The session-mode pooler (port 5432) is preferred. The transaction-mode
# pooler (port 6543) also works but logs benign "resetting connection"
# warnings.
# Format (session pooler):
# postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:5432/postgres
DATABASE_URL=
PGMQ_CHANNEL=pipecat_acme

View File

@@ -0,0 +1,268 @@
#
# Copyright (c) 2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Two LLM workers with per-worker TTS voices.
Same shape as ``local-handoff-two-agents.py``, but each child worker
runs its own TTS with a distinct voice. The main worker has no TTS —
audio comes from the child workers via the bus and is played by the
main worker's transport. Tasks announce the transfer ("let me connect
you with...") before handing off.
Architecture::
Main worker (no TTS):
transport.in → STT → user_agg → BusBridge → transport.out → assistant_agg
Child worker (with TTS):
bridge_in → LLM → TTS → bridge_out
Requirements:
- OPENAI_API_KEY
- DEEPGRAM_API_KEY
- CARTESIA_API_KEY
- DAILY_API_KEY (for Daily transport)
"""
import os
from dotenv import load_dotenv
from loguru import logger
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.bus import BusBridgeProcessor
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
LLMUserAggregatorParams,
)
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
from pipecat.services.cartesia.tts import CartesiaTTSService
from pipecat.services.deepgram.stt import DeepgramSTTService
from pipecat.services.llm_service import FunctionCallParams
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams
from pipecat.workers.llm import LLMWorker, LLMWorkerActivationArgs, tool
load_dotenv(override=True)
MAIN_NAME = "acme"
transport_params = {
"daily": lambda: DailyParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
"webrtc": lambda: TransportParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
}
class AcmeTTSTask(LLMWorker):
"""Child worker with its own LLM + TTS, bridged to the main worker.
Each child wraps the standard ``Pipeline([llm])`` with an extra
TTS processor so audio is produced locally by each child and
shipped to the main worker over the bus.
"""
def __init__(self, name: str, *, llm: OpenAILLMService, voice_id: str):
"""Initialize the child worker.
Args:
name: Unique worker name.
llm: The LLM service for this child.
voice_id: Cartesia voice id for this child's TTS.
"""
tts = CartesiaTTSService(
api_key=os.environ["CARTESIA_API_KEY"],
settings=CartesiaTTSService.Settings(voice=voice_id),
)
super().__init__(
name,
llm=llm,
pipeline=Pipeline([llm, tts]),
bridged=(),
)
@tool(cancel_on_interruption=False)
async def transfer_to_agent(self, params: FunctionCallParams, agent: str, reason: str):
"""Transfer the user to another agent.
Args:
agent (str): The agent to transfer to (e.g. 'greeter', 'support').
reason (str): Why the user is being transferred.
"""
logger.info(f"Task '{self.name}': transferring to '{agent}' ({reason})")
await self.activate_worker(
agent,
messages=[
{
"role": "developer",
"content": f"Tell the user about the transfer ({reason}).",
}
],
args=LLMWorkerActivationArgs(
messages=[{"role": "developer", "content": reason}],
),
deactivate_self=True,
result_callback=params.result_callback,
)
@tool
async def end_conversation(self, params: FunctionCallParams, reason: str):
"""End the conversation when the user says goodbye.
Args:
reason (str): Why the conversation is ending.
"""
logger.info(f"Task '{self.name}': ending conversation ({reason})")
await self.end(
reason=reason,
messages=[{"role": "developer", "content": reason}],
result_callback=params.result_callback,
)
def build_greeter() -> AcmeTTSTask:
"""Greeter: routes the user to support when they pick a product."""
llm = OpenAILLMService(
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(
system_instruction=(
"You are a friendly greeter for Acme Corp. The available products "
"are: the Acme Rocket Boots, the Acme Invisible Paint, and the Acme "
"Tornado Kit. Ask which one they'd like to learn more about. "
"When the user picks a product or asks a question about one, "
"call the transfer_to_agent tool with agent 'support'. "
"Do not answer product questions yourself. If the user says goodbye, "
"call the end_conversation tool. Keep responses brief — this is a "
"voice conversation."
),
),
)
return AcmeTTSTask(
"greeter",
llm=llm,
voice_id="9626c31c-bec5-4cca-baa8-f8ba9e84c8bc", # Jacqueline
)
def build_support() -> AcmeTTSTask:
"""Support: answers product questions, can hand back to the greeter."""
llm = OpenAILLMService(
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(
system_instruction=(
"You are a support agent for Acme Corp. You know about three "
"products: Acme Rocket Boots (jet-powered boots, $299, run up "
"to 60 mph), Acme Invisible Paint (makes anything invisible for "
"24 hours, $49 per can), and Acme Tornado Kit (portable tornado "
"generator, $199, batteries included). Answer the user's questions "
"about these products. If the user wants to browse other products "
"or start over, call the transfer_to_agent tool with agent "
"'greeter'. If the user says goodbye, call the end_conversation "
"tool. Keep responses brief — this is a voice conversation."
),
),
)
return AcmeTTSTask(
"support",
llm=llm,
voice_id="a167e0f3-df7e-4d52-a9c3-f949145efdab", # Blake
)
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
logger.info("Starting two-agents-with-tts bot")
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
stt = DeepgramSTTService(api_key=os.environ["DEEPGRAM_API_KEY"])
context = LLMContext()
aggregators = LLMContextAggregatorPair(
context,
user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()),
)
# The main worker has no TTS. Audio comes from the children over
# the bus; the main bridge tees user context out and pushes
# incoming audio/text frames back into the local pipeline.
bridge = BusBridgeProcessor(
bus=runner.bus,
worker_name=MAIN_NAME,
name=f"{MAIN_NAME}::BusBridge",
)
pipeline = Pipeline(
[
transport.input(),
stt,
aggregators.user(),
bridge,
transport.output(),
aggregators.assistant(),
]
)
worker = PipelineWorker(
pipeline,
name=MAIN_NAME,
params=PipelineParams(
enable_metrics=True,
enable_usage_metrics=True,
),
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
)
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
logger.info("Client connected")
await worker.activate_worker(
"greeter",
args=LLMWorkerActivationArgs(
messages=[
{
"role": "developer",
"content": (
"Welcome the user to Acme Corp, mention the available products "
"and ask how you can help."
),
},
],
),
)
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await runner.cancel()
await runner.add_workers(build_greeter(), build_support(), worker)
await runner.run()
async def bot(runner_args: RunnerArguments):
"""Main bot entry point compatible with Pipecat Cloud."""
transport = await create_transport(runner_args, transport_params)
await run_bot(transport, runner_args)
if __name__ == "__main__":
from pipecat.runner.run import main
main()

View File

@@ -0,0 +1,241 @@
#
# Copyright (c) 2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Two LLM workers with a main worker bridging transport to the bus.
Demonstrates multi-worker coordination: a main worker handles transport I/O
(STT, TTS) and bridges frames to the bus. Two LLM workers — a greeter and
a support worker — each run their own LLM pipeline and hand off control
between each other.
The user talks to one worker at a time. Hand-offs are seamless — the LLM
decides when to transfer based on its tools.
Requirements:
- OPENAI_API_KEY
- DEEPGRAM_API_KEY
- CARTESIA_API_KEY
- DAILY_API_KEY (for Daily transport)
"""
import os
from dotenv import load_dotenv
from loguru import logger
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.bus import BusBridgeProcessor
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
LLMUserAggregatorParams,
)
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
from pipecat.services.cartesia.tts import CartesiaTTSService
from pipecat.services.deepgram.stt import DeepgramSTTService
from pipecat.services.llm_service import FunctionCallParams
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams
from pipecat.workers.llm import LLMWorker, LLMWorkerActivationArgs, tool
load_dotenv(override=True)
MAIN_NAME = "acme"
transport_params = {
"daily": lambda: DailyParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
"webrtc": lambda: TransportParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
}
class AcmeLLMTask(LLMWorker):
"""LLM-only child worker with transfer/end tools.
Receives user context from the main worker via the bus, runs its LLM,
and ships generated text frames back. The main worker's TTS turns the
text into audio.
Passing ``bridged=()`` tells :class:`PipelineWorker` to wrap the LLM
pipeline with bus edge processors so frames flow between this worker
and the main worker automatically.
"""
@tool(cancel_on_interruption=False)
async def transfer_to_agent(self, params: FunctionCallParams, agent: str, reason: str):
"""Transfer the user to another agent.
Args:
agent (str): The agent to transfer to (e.g. 'greeter', 'support').
reason (str): Why the user is being transferred.
"""
logger.info(f"Task '{self.name}': transferring to '{agent}' ({reason})")
await self.activate_worker(
agent,
args=LLMWorkerActivationArgs(
messages=[{"role": "developer", "content": reason}],
),
deactivate_self=True,
result_callback=params.result_callback,
)
@tool
async def end_conversation(self, params: FunctionCallParams, reason: str):
"""End the conversation when the user says goodbye.
Args:
reason (str): Why the conversation is ending.
"""
logger.info(f"Task '{self.name}': ending conversation ({reason})")
await self.end(
reason=reason,
messages=[{"role": "developer", "content": reason}],
result_callback=params.result_callback,
)
def build_greeter() -> AcmeLLMTask:
"""Greeter: routes the user to support when they pick a product."""
llm = OpenAILLMService(
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(
system_instruction=(
"You are a friendly greeter for Acme Corp. The available products "
"are: the Acme Rocket Boots, the Acme Invisible Paint, and the Acme "
"Tornado Kit. Ask which one they'd like to learn more about. "
"When the user picks a product or asks a question about one, "
"immediately call the transfer_to_agent tool with agent 'support'. "
"Do not answer product questions yourself. If the user says goodbye, "
"call the end_conversation tool. Do not mention transferring — just do it "
"seamlessly. Keep responses brief — this is a voice conversation."
),
),
)
return AcmeLLMTask("greeter", llm=llm, bridged=())
def build_support() -> AcmeLLMTask:
"""Support: answers product questions, can hand back to the greeter."""
llm = OpenAILLMService(
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(
system_instruction=(
"You are a support agent for Acme Corp. You know about three "
"products: Acme Rocket Boots (jet-powered boots, $299, run up "
"to 60 mph), Acme Invisible Paint (makes anything invisible for "
"24 hours, $49 per can), and Acme Tornado Kit (portable tornado "
"generator, $199, batteries included). Answer the user's questions "
"about these products. If the user wants to browse other products "
"or start over, call the transfer_to_agent tool with agent "
"'greeter'. If the user says goodbye, call the end_conversation "
"tool. Do not mention transferring — just do it seamlessly. "
"Keep responses brief — this is a voice conversation."
),
),
)
return AcmeLLMTask("support", llm=llm, bridged=())
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
logger.info("Starting two-agent bot")
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
stt = DeepgramSTTService(api_key=os.environ["DEEPGRAM_API_KEY"])
tts = CartesiaTTSService(
api_key=os.environ["CARTESIA_API_KEY"],
settings=CartesiaTTSService.Settings(
voice="9626c31c-bec5-4cca-baa8-f8ba9e84c8bc", # Jacqueline
),
)
context = LLMContext()
aggregators = LLMContextAggregatorPair(
context,
user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()),
)
# The main bridge sends user-side context downstream to the children
# via the bus, and the children's generated text comes back here so
# the TTS can speak it.
bridge = BusBridgeProcessor(
bus=runner.bus,
worker_name=MAIN_NAME,
name=f"{MAIN_NAME}::BusBridge",
)
pipeline = Pipeline(
[
transport.input(),
stt,
aggregators.user(),
bridge,
tts,
transport.output(),
aggregators.assistant(),
]
)
worker = PipelineWorker(
pipeline,
name=MAIN_NAME,
params=PipelineParams(
enable_metrics=True,
enable_usage_metrics=True,
),
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
)
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
logger.info("Client connected")
await worker.activate_worker(
"greeter",
args=LLMWorkerActivationArgs(
messages=[
{
"role": "developer",
"content": (
"Welcome the user to Acme Corp, mention the available products "
"and ask how you can help."
),
},
],
),
)
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await runner.cancel()
await runner.add_workers(build_greeter(), build_support(), worker)
await runner.run()
async def bot(runner_args: RunnerArguments):
"""Main bot entry point compatible with Pipecat Cloud."""
transport = await create_transport(runner_args, transport_params)
await run_bot(transport, runner_args)
if __name__ == "__main__":
from pipecat.runner.run import main
main()

View File

@@ -0,0 +1,239 @@
#
# Copyright (c) 2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Parallel debate using job groups.
A voice bot receives a topic from the user and fans out to three
workers in parallel via ``worker.job_group(...)``. Each worker
runs its own LLM context, so it remembers previous topics across
debate rounds. The bot collects all three perspectives and the
main-worker LLM synthesizes a balanced answer.
Architecture::
Main worker (transport + LLM + ``debate`` tool)
└── job_group(advocate, critic, analyst)
└── DebateWorker (LLMContextWorker, one per role)
Requirements:
- OPENAI_API_KEY
- DEEPGRAM_API_KEY
- CARTESIA_API_KEY
- DAILY_API_KEY (for Daily transport)
"""
import os
from dotenv import load_dotenv
from loguru import logger
from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.bus import BusJobRequestMessage
from pipecat.frames.frames import LLMMessagesAppendFrame, LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
AssistantTurnStoppedMessage,
LLMContextAggregatorPair,
LLMUserAggregatorParams,
)
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
from pipecat.services.cartesia.tts import CartesiaTTSService
from pipecat.services.deepgram.stt import DeepgramSTTService
from pipecat.services.llm_service import FunctionCallParams
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams
from pipecat.workers.llm import LLMContextWorker
load_dotenv(override=True)
ROLE_PROMPTS = {
"advocate": (
"You argue IN FAVOR of the topic. Present the strongest case for why "
"this is a good idea, with concrete benefits. Be persuasive but honest. "
"Be concise, just 2-3 sentences."
),
"critic": (
"You argue AGAINST the topic. Present the strongest concerns, risks, "
"and downsides. Be critical but fair. Be concise, just 2-3 sentences."
),
"analyst": (
"You provide a BALANCED, NEUTRAL analysis. Weigh both sides objectively "
"and highlight the key trade-offs. Be concise, just 2-3 sentences."
),
}
transport_params = {
"daily": lambda: DailyParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
"webrtc": lambda: TransportParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
}
class DebateWorker(LLMContextWorker):
"""Worker that generates a perspective using its own LLM context.
Each worker keeps its own ``LLMContext`` so it remembers previous
topics across multiple debate rounds. Job requests append the new
topic and trigger the LLM; the assistant-aggregator captures the
full reply and sends it back as the job response.
"""
def __init__(self, role: str):
"""Initialize the DebateWorker.
Args:
role: One of ``"advocate"``, ``"critic"``, ``"analyst"`` —
used as the worker name and selects the system prompt.
"""
llm = OpenAILLMService(
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(system_instruction=ROLE_PROMPTS[role]),
)
super().__init__(role, llm=llm)
self._role = role
self._current_job_id: str | None = None
@self.assistant_aggregator.event_handler("on_assistant_turn_stopped")
async def on_assistant_turn_stopped(aggregator, message: AssistantTurnStoppedMessage):
text = message.content
logger.info(f"Worker '{self.name}': completed ({len(text)} chars)")
if self._current_job_id:
job_id = self._current_job_id
self._current_job_id = None
await self.send_job_response(job_id, {"role": self._role, "text": text})
async def on_job_request(self, message: BusJobRequestMessage) -> None:
"""Inject the topic and run the LLM."""
await super().on_job_request(message)
self._current_job_id = message.job_id
await self.queue_frame(
LLMMessagesAppendFrame(
messages=[{"role": "developer", "content": f"Topic: {message.payload['topic']}"}],
run_llm=True,
)
)
async def debate(params: FunctionCallParams, topic: str):
"""Analyze a topic from multiple perspectives (advocate, critic, analyst).
Args:
topic (str): The topic or question to debate.
"""
logger.info(f"Starting debate on '{topic}'")
async with params.pipeline_worker.job_group(
*ROLE_PROMPTS, payload={"topic": topic}, timeout=30
) as tg:
pass
result = "\n\n".join(f"{r['role'].upper()}: {r['text']}" for r in tg.responses.values())
logger.info("Debate complete, synthesizing")
await params.result_callback(result)
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
logger.info("Starting parallel-debate bot")
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
stt = DeepgramSTTService(api_key=os.environ["DEEPGRAM_API_KEY"])
tts = CartesiaTTSService(
api_key=os.environ["CARTESIA_API_KEY"],
settings=CartesiaTTSService.Settings(
voice="9626c31c-bec5-4cca-baa8-f8ba9e84c8bc", # Jacqueline
),
)
llm = OpenAILLMService(
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(
system_instruction=(
"You are a debate moderator in a voice conversation. When the user "
"gives you a topic, call the debate tool to gather perspectives from "
"three viewpoints (advocate, critic, analyst). Then synthesize the "
"results into a clear, balanced summary for the user. Keep your "
"responses concise and natural for speaking."
),
),
)
llm.register_direct_function(debate, cancel_on_interruption=False, timeout_secs=60)
context = LLMContext(tools=ToolsSchema(standard_tools=[debate]))
aggregators = LLMContextAggregatorPair(
context,
user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()),
)
pipeline = Pipeline(
[
transport.input(),
stt,
aggregators.user(),
llm,
tts,
transport.output(),
aggregators.assistant(),
]
)
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
enable_usage_metrics=True,
),
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
)
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
logger.info("Client connected")
context.add_message(
{
"role": "developer",
"content": (
"Greet the user and tell them you can moderate a debate on any "
"topic. Ask what they'd like to explore."
),
}
)
await worker.queue_frame(LLMRunFrame())
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await runner.cancel()
await runner.add_workers(
DebateWorker("advocate"),
DebateWorker("critic"),
DebateWorker("analyst"),
worker,
)
await runner.run()
async def bot(runner_args: RunnerArguments):
"""Main bot entry point compatible with Pipecat Cloud."""
transport = await create_transport(runner_args, transport_params)
await run_bot(transport, runner_args)
if __name__ == "__main__":
from pipecat.runner.run import main
main()

View File

@@ -0,0 +1,119 @@
#
# Copyright (c) 2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Remote assistant LLM server.
Runs a FastAPI server that accepts WebSocket connections from a
``main.py``-style client. Each connection spins up a
`WebSocketProxyServerTask` bridging the socket to a local
`PipelineRunner` and an `LLMWorker` that handles the conversation.
Usage::
python assistant.py
python assistant.py --port 9000
Requirements:
- OPENAI_API_KEY
"""
import argparse
import os
import uvicorn
from dotenv import load_dotenv
from fastapi import FastAPI, WebSocket
from loguru import logger
from pipecat.bus import BusFrameMessage
from pipecat.pipeline.runner import PipelineRunner
from pipecat.services.llm_service import FunctionCallParams
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.workers.llm import LLMWorker, tool
from pipecat.workers.proxy.websocket import WebSocketProxyServerTask
load_dotenv(override=True)
app = FastAPI()
class AcmeAssistant(LLMWorker):
"""Handles greetings, product questions, and conversation end."""
def __init__(self):
"""Initialize the AcmeAssistant LLM worker."""
llm = OpenAILLMService(
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(
system_instruction=(
"You are a friendly assistant for Acme Corp. You know about three "
"products: Acme Rocket Boots (jet-powered boots, $299, run up to "
"60 mph), Acme Invisible Paint (makes anything invisible for 24 hours, "
"$49 per can), and Acme Tornado Kit (portable tornado generator, $199, "
"batteries included). Greet the user, help them with product questions, "
"and call end_conversation when the user says goodbye. "
"Keep responses brief, this is a voice conversation."
),
),
)
super().__init__("assistant", llm=llm, bridged=())
@tool
async def end_conversation(self, params: FunctionCallParams, reason: str):
"""End the conversation when the user says goodbye.
Args:
reason (str): Why the conversation is ending.
"""
logger.info(f"Task '{self.name}': ending conversation ({reason})")
await self.end(
reason=reason,
messages=[{"role": "developer", "content": reason}],
result_callback=params.result_callback,
)
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""Handle a WebSocket connection from the main bot's proxy."""
await websocket.accept()
runner = PipelineRunner(handle_sigint=False)
proxy = WebSocketProxyServerTask(
"gateway",
websocket=websocket,
worker_name="assistant",
remote_worker_name="acme",
forward_messages=(BusFrameMessage,),
)
@proxy.event_handler("on_client_connected")
async def on_client_connected(proxy, client):
logger.info("WebSocket client connected")
@proxy.event_handler("on_client_disconnected")
async def on_client_disconnected(proxy, client):
logger.info("WebSocket client disconnected")
await runner.cancel()
assistant = AcmeAssistant()
await runner.add_workers(proxy, assistant)
logger.info("Assistant server ready, waiting for activation")
await runner.run()
logger.info("Assistant server session ended")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Remote assistant LLM server")
parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
parser.add_argument("--port", type=int, default=8765, help="Port to listen on")
args = parser.parse_args()
uvicorn.run(app, host=args.host, port=args.port)

View File

@@ -0,0 +1,170 @@
#
# Copyright (c) 2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Main transport worker with a WebSocket proxy to a remote LLM server.
Handles audio I/O (STT, TTS) and bridges frames to the bus. A
`WebSocketProxyClientTask` forwards bus messages to a remote LLM
server (see ``assistant.py``) over WebSocket.
Usage::
python main.py --remote-url ws://localhost:8765/ws
Requirements:
- DEEPGRAM_API_KEY
- CARTESIA_API_KEY
"""
import argparse
import os
from dotenv import load_dotenv
from loguru import logger
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.bus import BusBridgeProcessor, BusFrameMessage
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
LLMUserAggregatorParams,
)
from pipecat.registry.types import WorkerReadyData
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
from pipecat.services.cartesia.tts import CartesiaTTSService
from pipecat.services.deepgram.stt import DeepgramSTTService
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams
from pipecat.workers.llm import LLMWorkerActivationArgs
from pipecat.workers.proxy.websocket import WebSocketProxyClientTask
load_dotenv(override=True)
MAIN_NAME = "acme"
transport_params = {
"daily": lambda: DailyParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
"webrtc": lambda: TransportParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
}
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
stt = DeepgramSTTService(api_key=os.environ["DEEPGRAM_API_KEY"])
tts = CartesiaTTSService(
api_key=os.environ["CARTESIA_API_KEY"],
settings=CartesiaTTSService.Settings(
voice="9626c31c-bec5-4cca-baa8-f8ba9e84c8bc", # Jacqueline
),
)
context = LLMContext()
aggregators = LLMContextAggregatorPair(
context,
user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()),
)
bridge = BusBridgeProcessor(
bus=runner.bus,
worker_name=MAIN_NAME,
name=f"{MAIN_NAME}::BusBridge",
)
pipeline = Pipeline(
[
transport.input(),
stt,
aggregators.user(),
bridge,
tts,
transport.output(),
aggregators.assistant(),
]
)
worker = PipelineWorker(
pipeline,
name=MAIN_NAME,
params=PipelineParams(
enable_metrics=True,
enable_usage_metrics=True,
),
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
)
# Forward bus frame messages over the WebSocket so the remote
# assistant sees user-side context and can ship back its replies.
proxy = WebSocketProxyClientTask(
"proxy",
url=runner_args.cli_args.remote_url,
local_worker_name=MAIN_NAME,
remote_worker_name="assistant",
forward_messages=(BusFrameMessage,),
)
async def on_assistant_ready(_data: WorkerReadyData) -> None:
logger.info("Remote assistant ready, activating")
await worker.activate_worker(
"assistant",
args=LLMWorkerActivationArgs(
messages=[
{
"role": "developer",
"content": (
"Welcome the user to Acme Corp, mention the available "
"products and ask how you can help."
),
},
],
),
)
await runner.registry.watch("assistant", on_assistant_ready)
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
logger.info("Client connected, activating proxy")
await worker.activate_worker("proxy")
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await runner.cancel()
await runner.add_workers(proxy, worker)
await runner.run()
async def bot(runner_args: RunnerArguments):
"""Main bot entry point compatible with Pipecat Cloud."""
transport = await create_transport(runner_args, transport_params)
await run_bot(transport, runner_args)
if __name__ == "__main__":
from pipecat.runner.run import main
parser = argparse.ArgumentParser(description="Main transport worker with WebSocket proxy")
parser.add_argument(
"--remote-url",
default="ws://localhost:8765/ws",
help="WebSocket URL of the remote LLM server",
)
main(parser)

View File

@@ -0,0 +1,333 @@
#
# Copyright (c) 2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Voice agent + sensor-controller worker, both as plain PipelineTasks.
Two ``PipelineWorker`` instances run side by side:
- The **voice agent** is built inline in ``run_bot`` — a standard
transport + STT + LLM + TTS pipeline. Its LLM has a single tool,
``ask_controller(question)``, which forwards the user's request to
the controller over the bus and speaks back the response.
- The **sensor controller** (``build_sensor_controller``) is a
``PipelineWorker`` whose pipeline runs a simulated temperature sensor
(see ``sensor.py``) alongside its own LLM. The worker LLM has tool
access to read the current reading, inspect rolling stats, and
mutate the simulated sensor's target temperature and response rate.
The worker does **not** subclass ``LLMWorker`` and is **not** bridged.
The voice agent and the controller communicate exclusively through
``BusJobRequestMessage`` / ``BusJobResponseMessage``. The controller
collects responses by listening to the assistant aggregator's
``on_assistant_turn_stopped`` event and pairing each LLM completion
with the in-flight job id.
Requirements:
- OPENAI_API_KEY
- DEEPGRAM_API_KEY
- CARTESIA_API_KEY
- DAILY_API_KEY (for Daily transport)
Example voice exchange::
User: What's the temperature?
Controller: 22.1°C, holding steady.
User: Make it warmer.
Controller: I set the target to 26°C. Give it about 20 seconds.
User: Is it stable yet?
Controller: It's at 25.4°C and still climbing — almost there.
User: Why is it slow?
Controller: The response rate is 5%. I sped it up to 20%; it'll settle faster now.
"""
import os
from dotenv import load_dotenv
from loguru import logger
from sensor import SensorReader, SensorStats
from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.bus import BusJobRequestMessage
from pipecat.frames.frames import LLMMessagesAppendFrame, LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
AssistantTurnStoppedMessage,
LLMContextAggregatorPair,
LLMUserAggregatorParams,
)
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
from pipecat.services.cartesia.tts import CartesiaTTSService
from pipecat.services.deepgram.stt import DeepgramSTTService
from pipecat.services.llm_service import FunctionCallParams
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams
load_dotenv(override=True)
transport_params = {
"daily": lambda: DailyParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
"webrtc": lambda: TransportParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
}
def build_sensor_controller() -> PipelineWorker:
"""Build the controller worker as a plain :class:`PipelineWorker`.
The pipeline shape is::
SensorReader -> SensorStats -> user_agg -> llm -> assistant_agg
``SensorReader`` runs an autonomous tick loop that emits a
:class:`SensorReadingFrame` every second; ``SensorStats`` consumes
those readings and exposes rolling statistics. The LLM has four
direct tools that read or mutate the sensor.
Jobs arrive via the ``on_job_request`` event handler. The handler
stores the active ``job_id``, then queues an
:class:`LLMMessagesAppendFrame` with the user's question and runs
the LLM. When the assistant turn finishes (signalled by the
assistant aggregator's ``on_assistant_turn_stopped`` event), the
handler sends a :class:`BusJobResponseMessage` carrying the LLM's
answer back to the voice agent.
"""
sensor = SensorReader()
stats = SensorStats()
async def get_current_reading(params: FunctionCallParams):
"""Read the sensor's current temperature in degrees Celsius."""
await params.result_callback({"temperature": round(sensor.current, 2)})
async def get_stats(params: FunctionCallParams):
"""Rolling minimum, maximum, average, and trend of the temperature."""
await params.result_callback(
{
"min": round(stats.min, 2),
"max": round(stats.max, 2),
"avg": round(stats.avg, 2),
"trend": stats.trend,
}
)
async def set_target_temperature(params: FunctionCallParams, target_celsius: float):
"""Adjust the target temperature; the sensor will drift toward it.
Args:
target_celsius (float): The new target temperature in degrees Celsius.
"""
sensor.set_target(target_celsius)
await params.result_callback({"ok": True, "new_target": target_celsius})
async def set_response_rate(params: FunctionCallParams, rate: float):
"""Set how aggressively the sensor approaches the target.
Args:
rate (float): Response rate between 0.01 (slow) and 0.3 (fast).
"""
sensor.set_response_rate(rate)
await params.result_callback({"ok": True, "new_rate": rate})
llm = OpenAILLMService(
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(
system_instruction=(
"You are a temperature sensor controller. You manage a single "
"thermometer and answer the user's questions about it. Use the "
"provided tools to read the current temperature, inspect rolling "
"statistics, change the target temperature, or change how fast "
"the sensor responds. When the user asks for a vague change "
"('make it warmer', 'cooler'), pick a sensible target and call "
"set_target_temperature. Always answer in one or two short "
"sentences — your reply is spoken aloud."
),
),
)
llm.register_direct_function(get_current_reading)
llm.register_direct_function(get_stats)
llm.register_direct_function(set_target_temperature)
llm.register_direct_function(set_response_rate)
context = LLMContext(
tools=ToolsSchema(
standard_tools=[
get_current_reading,
get_stats,
set_target_temperature,
set_response_rate,
]
)
)
aggregators = LLMContextAggregatorPair(context)
pipeline = Pipeline(
[
sensor,
stats,
aggregators.user(),
llm,
aggregators.assistant(),
]
)
worker = PipelineWorker(pipeline, name="controller")
# The controller handles one job at a time (the LLM pipeline can only
# run one turn at a time). ``state["job_id"]`` pairs the in-flight
# job with the next ``on_assistant_turn_stopped`` event.
state: dict[str, str | None] = {"job_id": None}
@worker.event_handler("on_job_request")
async def on_request(_task, message: BusJobRequestMessage):
question = message.payload["question"]
logger.info(f"Controller: received question '{question}'")
state["job_id"] = message.job_id
await worker.queue_frame(
LLMMessagesAppendFrame(
messages=[{"role": "user", "content": question}],
run_llm=True,
)
)
@aggregators.assistant().event_handler("on_assistant_turn_stopped")
async def on_assistant_turn_stopped(_aggregator, message: AssistantTurnStoppedMessage):
# The aggregator fires this event on every ``LLMFullResponseEndFrame``,
# including the tool-call round that precedes the tool result and has
# no spoken text. Skip those so we only forward the LLM's final
# response to the voice agent.
if not message.content:
return
if state["job_id"] is None:
return
job_id, state["job_id"] = state["job_id"], None
logger.info(f"Controller: answering job {job_id[:8]}")
await worker.send_job_response(job_id, response={"answer": message.content})
return worker
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
logger.info("Starting sensor-controller bot")
# Voice agent: standard transport + STT + LLM + TTS pipeline. The
# only tool the voice LLM has is ``ask_controller`` — it does not
# know anything about temperatures, trends, or response rates.
stt = DeepgramSTTService(api_key=os.environ["DEEPGRAM_API_KEY"])
tts = CartesiaTTSService(
api_key=os.environ["CARTESIA_API_KEY"],
settings=CartesiaTTSService.Settings(
voice="9626c31c-bec5-4cca-baa8-f8ba9e84c8bc", # Jacqueline
),
)
async def ask_controller(params: FunctionCallParams, question: str):
"""Ask the temperature sensor controller anything about the sensor.
Forward the user's request verbatim and speak back the answer.
Args:
question (str): The user's question or instruction to forward to the controller.
"""
logger.info(f"Voice agent: forwarding to controller: '{question}'")
async with params.pipeline_worker.job(
"controller", payload={"question": question}, timeout=30
) as t:
pass
await params.result_callback(t.response["answer"])
llm = OpenAILLMService(
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(
system_instruction=(
"You are a friendly voice assistant with access to a temperature "
"sensor controller. For ANY request about the temperature — "
"reading it, adjusting it, checking trends, changing how fast it "
"responds — call the ask_controller tool. Forward the user's "
"request verbatim. Then speak the controller's answer back. "
"Keep responses brief; do not add extra commentary."
),
),
)
llm.register_direct_function(ask_controller, timeout_secs=60)
context = LLMContext(tools=ToolsSchema(standard_tools=[ask_controller]))
aggregators = LLMContextAggregatorPair(
context,
user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()),
)
pipeline = Pipeline(
[
transport.input(),
stt,
aggregators.user(),
llm,
tts,
transport.output(),
aggregators.assistant(),
]
)
worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
enable_usage_metrics=True,
),
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
)
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
logger.info("Client connected")
context.add_message(
{
"role": "developer",
"content": (
"Greet the user and let them know you can read or adjust a "
"temperature sensor on their behalf."
),
}
)
await worker.queue_frame(LLMRunFrame())
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await runner.cancel()
await runner.add_workers(build_sensor_controller(), worker)
await runner.run()
async def bot(runner_args: RunnerArguments):
"""Main bot entry point compatible with Pipecat Cloud."""
transport = await create_transport(runner_args, transport_params)
await run_bot(transport, runner_args)
if __name__ == "__main__":
from pipecat.runner.run import main
main()

View File

@@ -0,0 +1,186 @@
#
# Copyright (c) 2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Temperature sensor processors for the sensor-controller example.
Two custom :class:`FrameProcessor` subclasses that give the worker
pipeline real autonomous frame flow:
- :class:`SensorReader` simulates a thermometer. It runs an async tick
loop that advances ``current`` toward ``target`` with a first-order
lag plus Gaussian noise, and pushes a :class:`SensorReadingFrame` on
every tick. ``target`` and ``response_rate`` are mutable so the
worker's LLM can adjust them via tool calls.
- :class:`SensorStats` consumes the readings, maintains a rolling
window, and exposes ``current`` / ``min`` / ``max`` / ``avg`` /
``trend`` as properties. The worker LLM reads these directly when
answering the user.
"""
import random
import time
from collections import deque
from dataclasses import dataclass
from pipecat.frames.frames import DataFrame, Frame, StartFrame
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
@dataclass
class SensorReadingFrame(DataFrame):
"""A single temperature reading emitted by :class:`SensorReader`.
Parameters:
temperature: The reading in degrees Celsius.
timestamp: Unix timestamp when the reading was taken.
"""
temperature: float = 0.0
timestamp: float = 0.0
class SensorReader(FrameProcessor):
"""Simulated temperature sensor with adjustable target and response rate.
Each tick, ``current`` is updated as::
current += (target - current) * response_rate + gauss(0, noise_sigma)
This is a first-order lag toward ``target``. With ``response_rate=0.05``
and a 1s tick, the current reading reaches ~halfway to target in ~14s;
with ``response_rate=0.2`` it converges in ~510s.
"""
def __init__(
self,
*,
start_temp: float = 22.0,
sample_period_s: float = 1.0,
response_rate: float = 0.05,
noise_sigma: float = 0.1,
):
"""Initialize the sensor.
Args:
start_temp: Initial temperature and initial target (°C).
sample_period_s: Seconds between successive readings.
response_rate: Fraction of the gap toward target closed each tick
(clamped to ``[0.0, 1.0]``).
noise_sigma: Standard deviation of the Gaussian noise added to
each reading.
"""
super().__init__()
self._current = start_temp
self._target = start_temp
self._response_rate = max(0.0, min(1.0, response_rate))
self._noise_sigma = noise_sigma
self._sample_period_s = sample_period_s
self._tick_task = None
@property
def current(self) -> float:
"""The most recent temperature reading (°C)."""
return self._current
@property
def target(self) -> float:
"""The temperature the sensor is drifting toward (°C)."""
return self._target
@property
def response_rate(self) -> float:
"""Fraction of the target-current gap closed per tick."""
return self._response_rate
def set_target(self, value: float) -> None:
"""Set a new target temperature (°C)."""
self._target = value
def set_response_rate(self, rate: float) -> None:
"""Set how aggressively the sensor approaches the target.
Args:
rate: Fraction in ``[0.0, 1.0]``. Clamped to that range.
"""
self._response_rate = max(0.0, min(1.0, rate))
async def process_frame(self, frame: Frame, direction: FrameDirection):
await super().process_frame(frame, direction)
if isinstance(frame, StartFrame) and self._tick_task is None:
self._tick_task = self.create_task(self._tick_loop(), "ticker")
await self.push_frame(frame, direction)
async def cleanup(self) -> None:
if self._tick_task is not None:
await self.cancel_task(self._tick_task)
self._tick_task = None
await super().cleanup()
async def _tick_loop(self) -> None:
import asyncio
while True:
await asyncio.sleep(self._sample_period_s)
gap = self._target - self._current
self._current += gap * self._response_rate + random.gauss(0, self._noise_sigma)
await self.push_frame(
SensorReadingFrame(temperature=self._current, timestamp=time.time()),
FrameDirection.DOWNSTREAM,
)
class SensorStats(FrameProcessor):
"""Rolling-window statistics over :class:`SensorReadingFrame`s.
Consumes readings as they flow downstream and exposes rolling
``min`` / ``max`` / ``avg`` / ``trend`` as properties — the worker
LLM reads them directly when responding to the user.
"""
def __init__(self, window: int = 30):
"""Initialize the stats aggregator.
Args:
window: Number of recent readings to retain.
"""
super().__init__()
self._readings: deque[float] = deque(maxlen=window)
@property
def current(self) -> float:
"""The most recent reading, or 0.0 if none have been seen."""
return self._readings[-1] if self._readings else 0.0
@property
def min(self) -> float:
return min(self._readings) if self._readings else 0.0
@property
def max(self) -> float:
return max(self._readings) if self._readings else 0.0
@property
def avg(self) -> float:
return sum(self._readings) / len(self._readings) if self._readings else 0.0
@property
def trend(self) -> str:
"""``"rising"`` / ``"falling"`` / ``"stable"`` based on first vs. last half of the window."""
if len(self._readings) < 4:
return "stable"
half = len(self._readings) // 2
old_avg = sum(list(self._readings)[:half]) / half
new_avg = sum(list(self._readings)[half:]) / (len(self._readings) - half)
diff = new_avg - old_avg
if abs(diff) < 0.25:
return "stable"
return "rising" if diff > 0 else "falling"
async def process_frame(self, frame: Frame, direction: FrameDirection):
await super().process_frame(frame, direction)
if isinstance(frame, SensorReadingFrame):
self._readings.append(frame.temperature)
await self.push_frame(frame, direction)

View File

@@ -0,0 +1,90 @@
# async-tasks
The UIWorker fans out long-running work to multiple peer workers in
parallel, streams their progress to an in-flight panel on the page, and
lets the user cancel mid-flight.
## What it shows
- The **`user_job_group` / `start_user_job_group`** API: dispatching
parallel work to multiple peer workers and automatically forwarding
every job lifecycle event to the client. The `reply` tool calls
`start_user_job_group("wikipedia", "news", "scholar", payload=...,
label=...)` and the `UIWorker` does the rest.
- The four **`ui-task` envelopes** the worker forwards (`group_started`,
`task_update`, `task_completed`, `group_completed`) and the
client-side `RTVIEvent.UITask` event for consuming them. The client
keeps a state map keyed by `task_id` and renders per-worker progress.
- **Cancellation**: the in-flight card's Cancel button calls
`client.cancelUITask(task_id, reason)`. The reserved `__cancel_task`
event is translated by the `UIWorker` into `cancel_job_group(task_id)`
on the registered group; cancelled workers report status `cancelled`.
- **Background dispatch from a tool**: `start_user_job_group` returns
immediately so the `reply` tool can speak its acknowledgement
("Researching the Mariana Trench now") while the workers run — the
main LLM is free to take follow-up turns.
## What it adds vs. the prior demos
The other examples use the request/response half of the bus protocol
(main LLM → UIWorker → reply). This one adds the streaming job-group
half: UIWorker → peer workers → progress events forwarded to the client.
The architecture grows from "one delegate" to "one delegate plus a
worker pool" — the peers are plain `BaseWorker`s launched on the runner.
## Run
Two terminals.
**Terminal 1 — bot:**
```bash
cd examples/multi-worker/ui-worker/async-tasks
uv run python bot.py
```
The bot starts on `http://localhost:7860`.
**Terminal 2 — client:**
```bash
cd examples/multi-worker/ui-worker/async-tasks/client
npm install # one-time
npm run dev
```
Open `http://localhost:5173` and click **Connect**.
## What to try
The workers are simulated (canned summaries, randomized `asyncio.sleep`
delays) so the demo focuses on the protocol, not the AI. Each research
call takes a few seconds.
- _"Research the Mariana Trench."_ — the worker spawns three peers,
acknowledges in one short reply, and a card appears showing each
peer's status as it progresses (searching → found N results →
summarizing → completed).
- _"Look up octopus cognition."_ — same flow; a second card stacks.
- _"Research the moon, then research Mars."_ — two groups run
concurrently.
- _"How are you?"_ (no research) — quick reply, no job group.
- **Click Cancel on an in-flight card** — the cancellation routes
through, the peers' tasks raise `CancelledError`, and their responses
come back as `cancelled`.
## Requirements
- `OPENAI_API_KEY`
- `DEEPGRAM_API_KEY`
- `CARTESIA_API_KEY`
A `.env` in the example folder is the easiest way to set these (see
`examples/multi-worker/env.example`).
## What this example _doesn't_ show
Real worker integrations (the peers are simulated), LLM-driven peers
(these are pure data-fetch — a peer can itself be an `LLMWorker`),
streaming chunks (`send_job_stream_data` for progressive output), or
worker-to-worker fan-out (nested job groups).

Some files were not shown because too many files have changed in this diff Show More