Files
pipecat/examples/multi-task/local-handoff/local-handoff-two-agents.py
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

243 lines
8.2 KiB
Python

#
# Copyright (c) 2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Two LLM tasks with a main task bridging transport to the bus.
Demonstrates multi-task coordination: a main task handles transport I/O
(STT, TTS) and bridges frames to the bus. Two LLM tasks — a greeter and
a support task — each run their own LLM pipeline and hand off control
between each other.
The user talks to one task 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.task import PipelineParams, PipelineTask
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.tasks.llm import LLMTask, LLMTaskActivationArgs, tool
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams
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(LLMTask):
"""LLM-only child task with transfer/end tools.
Receives user context from the main task via the bus, runs its LLM,
and ships generated text frames back. The main task's TTS turns the
text into audio.
Passing ``bridged=()`` tells :class:`PipelineTask` to wrap the LLM
pipeline with bus edge processors so frames flow between this task
and the main task 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.handoff_to(
agent,
activation_args=LLMTaskActivationArgs(
messages=[{"role": "developer", "content": reason}],
),
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,
task_name=MAIN_NAME,
name=f"{MAIN_NAME}::BusBridge",
)
pipeline = Pipeline(
[
transport.input(),
stt,
aggregators.user(),
bridge,
tts,
transport.output(),
aggregators.assistant(),
]
)
task = PipelineTask(
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 task.activate_task(
"greeter",
args=LLMTaskActivationArgs(
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.spawn(build_greeter())
await runner.spawn(build_support())
await runner.spawn(task)
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()