Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
987f134
Add OpenAILiveLLMService with Responses delegation
kompfner Aug 28, 2026
edf94c3
Support persisted contexts with OpenAILiveLLMService.reset_conversati…
kompfner Aug 28, 2026
da43196
Add client delegation via a reusable BackendLLMWorker
kompfner Aug 28, 2026
5a97f9f
Add a cascade-frontend example of delegating to a BackendLLMWorker
kompfner Aug 28, 2026
5008d63
Treat Live output audio as a speech stream and report backend token u…
kompfner Aug 28, 2026
f7251a5
Record user turns as they end and drop the old session on reset
kompfner Aug 28, 2026
37e01f5
Name the live model's role in the two-layer architecture in the servi…
kompfner Aug 28, 2026
f380e76
Stream only intermediate backend responses as job updates
kompfner Aug 28, 2026
6394487
Stream the backend's reasoning summaries as thought updates
kompfner Aug 28, 2026
fe83f93
Tell LLMs to deliver an async-tool result on its own once the user is…
kompfner Aug 28, 2026
5d35e18
Treat backend progress as silent context
kompfner Aug 28, 2026
4a83fe2
Fix pyright errors in the Live service
kompfner Aug 31, 2026
1a68572
Rename the Realtime video example so it doesn't read as a Live example
kompfner Sep 1, 2026
68132c9
Give the backend its instructions via Settings, like the other examples
kompfner Sep 1, 2026
3cc5fa9
Restate the async-tool guidance as a minimal edit of its tuned wording
kompfner Sep 1, 2026
e44db99
Log backend updates and the final result in the cascade example
kompfner Sep 1, 2026
3f750f0
Give backend output a type, and let an app decide what the user hears
kompfner Sep 2, 2026
d5e8620
Speak the Live API's v3 contract
kompfner Sep 2, 2026
7ec07fd
Teach the examples the v3 delegation contract
kompfner Sep 2, 2026
2ca27ca
Show the two-tier split with a speech-to-speech frontend too
kompfner Sep 2, 2026
2b4e291
Note the backend output contract in the changelog
kompfner Sep 2, 2026
bc7506d
In `BackendLLMWorker`, go back to defaulting to only marking the fina…
kompfner Sep 3, 2026
522c4f0
Let speakable decide whether a roll-your-own frontend says it now
kompfner Sep 3, 2026
b0dcb62
Delegate to a model worth delegating to
kompfner Sep 3, 2026
61c8a75
Hand the backend whole utterances, not frame slices
kompfner Sep 3, 2026
0a1cafa
Give the backend's request a type of its own
kompfner Sep 3, 2026
6a7e1ba
Take the conversation as context messages, whatever a frontend has
kompfner Sep 3, 2026
c9b360c
Say what the conversation is, and that text-only is a choice
kompfner Sep 3, 2026
d7c4c2a
Let the frontend say what it wants, in its own words
kompfner Sep 4, 2026
e0c598c
Introduce the transcript by the delegation it follows
kompfner Sep 4, 2026
579b231
Call a delegation a delegation
kompfner Sep 4, 2026
41a8dc4
Say what the code does, without the road not taken
kompfner Sep 4, 2026
fc67feb
Name the speech example for the markers it turns on
kompfner Sep 4, 2026
0c6a44e
Name the example for the choice it demonstrates
kompfner Sep 4, 2026
13045a4
Cover a delegation that takes more than one tool round
kompfner Sep 4, 2026
7a0a6d4
Name the caller side for the act, not the transport
kompfner Sep 4, 2026
3e44527
Say what the completion check assumes about tool results
kompfner Sep 4, 2026
15383ef
Let the speech-to-speech frontend word its own request
kompfner Sep 4, 2026
a6c0986
Take only the backend's answer, with a realtime frontend
kompfner Sep 4, 2026
7ff054c
Drop the client-delegation persistent-context example
kompfner Sep 4, 2026
da221ca
Let the API pick the voice when an app doesn't
kompfner Sep 4, 2026
011d845
Cover the transcript preamble across a reset
kompfner Sep 4, 2026
cb64383
Render a transcript from what a context actually holds
kompfner Sep 4, 2026
9d8968b
Leave the handoff out of a client's tool-call events
kompfner Sep 4, 2026
0b22491
Keep the backend request helpers internal
kompfner Sep 9, 2026
1774f2f
Describe the backend worker by what delegates to it
kompfner Sep 9, 2026
bfaeaf5
Drop the roll-your-own two-tier examples
kompfner Sep 9, 2026
c16c6cd
Drop the async-tool guidance change
kompfner Sep 9, 2026
428eee4
Mark whole messages as speakable, not lines
kompfner Sep 9, 2026
1173de6
Give the speakable-updates example a job worth narrating
kompfner Sep 9, 2026
50463b9
Say what to ask the speakable-updates example
kompfner Sep 9, 2026
4674f55
Print the suggested request when a client connects
kompfner Sep 9, 2026
76e7b0d
Make the suggested request stand out on connect
kompfner Sep 9, 2026
3f8a350
Name the backend's speech flag for the hint it is
kompfner Sep 9, 2026
33873d8
Name who weighs the backend's speech hint
kompfner Sep 9, 2026
6f12012
Give each addition its own changelog fragment
kompfner Sep 9, 2026
c57903c
Fix three defects found in review
kompfner Sep 10, 2026
0cf6152
Let the payload's flags default where the fields do
kompfner Sep 10, 2026
b5d81f4
Fix four more defects found in review
kompfner Sep 10, 2026
dda1f58
Judge a startup failure per connection, not per service
kompfner Sep 10, 2026
5670adf
Apply the turn gap to the session in progress
kompfner Sep 10, 2026
87f1d9a
Fix the turn gap rather than exposing it
kompfner Sep 10, 2026
485a9e8
Warn about unhandled setting updates the shared way
kompfner Sep 10, 2026
4184bbe
Close the last five defects found in review
kompfner Sep 10, 2026
cb342ae
Send a tools update when something says the tools changed
kompfner Sep 10, 2026
9d8978c
Speak to the GA Live API
kompfner Sep 10, 2026
77cb8c0
Describe how a user turn reaches the context
kompfner Sep 10, 2026
45172bb
Name the changelog fragments after the PR
kompfner Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/5688.added.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added `BackendLLMWorker` and `BackendOutput` (`pipecat.workers.llm`): a worker that runs any LLM service, with its own context and multi-step tool calling, as the backend `OpenAILiveLLMService` client delegation hands work to. Everything the backend produces comes back as a `BackendOutput` saying what it is and whether the user may hear it, and `transform_output` decides that per output, or rewrites the text on its way out — see `examples/realtime/realtime-openai-live-client-delegation-spoken-updates.py`.
1 change: 1 addition & 0 deletions changelog/5688.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added `OpenAILiveLLMService`, a speech-to-speech service for the OpenAI Live API (`gpt-live-1`). The live model is full-duplex — it listens and speaks at the same time and handles being interrupted itself — and delegates search, reasoning and tool use to a backend model while the conversation continues. Both delegation modes are supported: `OpenAILiveLLMService.ResponsesDelegation` lets OpenAI host the backend (Responses API) model, with its function calls executed by the pipeline's registered tool handlers; `OpenAILiveLLMService.ClientDelegation` runs any Pipecat LLM service as the backend through a `BackendLLMWorker`. See `examples/realtime/realtime-openai-live-responses-delegation.py` and `examples/realtime/realtime-openai-live-client-delegation.py`, plus `examples/realtime/realtime-openai-live-client-delegation-spoken-updates.py` for a backend that chooses what the user hears.
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#

"""OpenAI Live (gpt-live-1) with Responses delegation and a persisted context.

The conversation recorded in the ``LLMContext`` can be saved to a file and
loaded back into the session with tools the delegated Responses model calls.
Loading restarts the Live session with the restored history as its prior
conversation, since the Live API only takes history at session start.
"""

import asyncio
import glob
import json
import os
from datetime import datetime

from dotenv import load_dotenv
from loguru import logger

from pipecat.evals.transport import EvalTransportParams
from pipecat.frames.frames import LLMRunFrame
from pipecat.observers.loggers.transcription_log_observer import TranscriptionLogObserver
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.worker import PipelineParams, PipelineWorker, ProcessorUnusablePolicy
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
from pipecat.services.llm_service import FunctionCallParams
from pipecat.services.openai.live.llm import OpenAILiveLLMService
from pipecat.services.openai.responses.llm import OpenAIResponsesLLMService
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams
from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams
from pipecat.workers.runner import WorkerRunner

load_dotenv(override=True)

BASE_FILENAME = "/tmp/pipecat_conversation_"

FRONTEND_INSTRUCTIONS = """## Role and speaking style
You are a friendly, concise voice assistant. Speak naturally, in one or two
sentences at a time, and let the user finish before responding.

## Delegation
Answer simple conversational questions directly. Delegate when the user asks
for current information such as the weather, or asks you to save the
conversation, list saved conversations, or load one. When delegating, include
the user's goal and the exact details they gave, so the request is
self-contained. Relay the result once it arrives.

## Interruptions
Stop speaking when the user interrupts and listen to the new request."""

BACKEND_INSTRUCTIONS = """You are helping an assistant during a live voice
conversation. The request may contain transcription errors; use the most
likely intent. Use the available tools to answer questions about the weather
and to save, list and load conversations. Return the verified result in
concise, conversational plain text — no Markdown, no raw JSON."""


async def get_current_weather(params: FunctionCallParams, location: str, format: str):
"""Get the current weather.

Args:
location: The city and state, e.g. "San Francisco, CA".
format: The temperature unit to use. Must be either "celsius" or "fahrenheit". Infer this from the user's location.
"""
temperature = 75 if format == "fahrenheit" else 24
await params.result_callback(
{
"conditions": "nice",
"temperature": temperature,
"format": format,
"timestamp": datetime.now().strftime("%Y%m%d_%H%M%S"),
}
)


async def get_saved_conversation_filenames(params: FunctionCallParams):
"""Get a list of saved conversation histories. Returns a list of filenames. Each filename includes a date and timestamp. Each file is conversation history that can be loaded into this session."""
matching_files = glob.glob(f"{BASE_FILENAME}*.json")
logger.debug(f"matching files: {matching_files}")
await params.result_callback({"filenames": matching_files})


async def save_conversation(params: FunctionCallParams):
"""Save the current conversation. Use this function to persist the current conversation to external storage."""
timestamp = datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
filename = f"{BASE_FILENAME}{timestamp}.json"
# The context holds the conversation as recorded from the Live session's
# transcripts, plus the backend's tool calls.
messages = params.context.get_messages()
logger.debug(f"writing conversation to {filename}\n{json.dumps(messages, indent=4)}")
try:
with open(filename, "w") as file:
json.dump(messages, file, indent=2)
await params.result_callback({"success": True, "filename": filename})
except Exception as e:
await params.result_callback({"success": False, "error": str(e)})


async def load_conversation(params: FunctionCallParams, filename: str):
"""Load a conversation history. Use this function to load a conversation history into the current session.

Args:
filename: The filename of the conversation history to load.
"""

async def _reset():
logger.debug(f"loading conversation from {filename}")
try:
with open(filename) as file:
params.context.set_messages(json.load(file))
params.context.add_message(
{
"role": "developer",
"content": "The saved conversation above has just been restored. Briefly "
"tell the user it's loaded and that you're ready to continue.",
}
)
assert isinstance(params.llm, OpenAILiveLLMService)
# The new session seeds itself from the restored context. The
# result of this tool call is deliberately not reported: the call
# belonged to the session that just ended.
await params.llm.reset_conversation()
except Exception as e:
await params.result_callback({"success": False, "error": str(e)})

asyncio.create_task(_reset())


# We use lambdas to defer transport parameter creation until the transport
# type is selected at runtime.
transport_params = {
"eval": lambda: EvalTransportParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
"daily": lambda: DailyParams(
audio_in_enabled=True,
audio_out_enabled=True,
),
"twilio": lambda: FastAPIWebsocketParams(
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):
logger.info("Starting bot")

llm = OpenAILiveLLMService(
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILiveLLMService.Settings(system_instruction=FRONTEND_INSTRUCTIONS),
delegation=OpenAILiveLLMService.ResponsesDelegation(
settings=OpenAIResponsesLLMService.Settings(
model="gpt-5.6-terra",
system_instruction=BACKEND_INSTRUCTIONS,
reasoning=OpenAIResponsesLLMService.ReasoningConfig(effort="low"),
),
),
)

context = LLMContext(
[{"role": "developer", "content": "Greet the user and ask how you can help."}],
[
get_current_weather,
save_conversation,
get_saved_conversation_filenames,
load_conversation,
],
)
user_aggregator, assistant_aggregator = LLMContextAggregatorPair(context)

pipeline = Pipeline(
[
transport.input(), # Transport user input
user_aggregator,
llm, # LLM
transport.output(), # Transport bot output
assistant_aggregator,
]
)

worker = PipelineWorker(
pipeline,
params=PipelineParams(
enable_metrics=True,
enable_usage_metrics=True,
),
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
observers=[TranscriptionLogObserver()],
processor_unusable_policy=ProcessorUnusablePolicy.END,
)

runner = WorkerRunner(handle_sigint=runner_args.handle_sigint)

await runner.add_workers(worker)

@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
logger.info("Client connected")
# Start the Live session from the context.
await worker.queue_frames([LLMRunFrame()])

@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info("Client disconnected")
await runner.cancel()

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()
Loading
Loading