pipecat bot example before trace

This commit is contained in:
vipyne
2025-10-23 13:38:02 -05:00
parent ab58f72322
commit fd0ef110ff
4 changed files with 356 additions and 0 deletions

View File

@@ -0,0 +1,159 @@
#
# Copyright (c) 20242025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import os
from dotenv import load_dotenv
from loguru import logger
from turn_detector_observer import TurnDetectorObserver
from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams
from pipecat.audio.turn.smart_turn.local_smart_turn_v3 import LocalSmartTurnAnalyzerV3
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.audio.vad.vad_analyzer import VADParams
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.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.cartesia.stt import CartesiaSTTService
from pipecat.services.cartesia.tts import CartesiaTTSService
from pipecat.services.elevenlabs.tts import ElevenLabsTTSService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.openai.stt import OpenAISTTService
from pipecat.services.openai.tts import OpenAITTSService
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams
from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams
load_dotenv(override=True)
# We store functions so objects (e.g. SileroVADAnalyzer) don't get
# instantiated. The function will be called when the desired transport gets
# selected.
transport_params = {
"daily": lambda: DailyParams(
audio_in_enabled=True,
audio_out_enabled=True,
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.2)),
turn_analyzer=LocalSmartTurnAnalyzerV3(params=SmartTurnParams()),
),
"twilio": lambda: FastAPIWebsocketParams(
audio_in_enabled=True,
audio_out_enabled=True,
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.2)),
turn_analyzer=LocalSmartTurnAnalyzerV3(params=SmartTurnParams()),
),
"webrtc": lambda: TransportParams(
audio_in_enabled=True,
audio_out_enabled=True,
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.2)),
turn_analyzer=LocalSmartTurnAnalyzerV3(params=SmartTurnParams()),
),
}
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
logger.info(f"Starting bot")
### STT ###
stt = OpenAISTTService(
api_key=os.getenv("OPENAI_API_KEY"),
model="gpt-4o-transcribe",
prompt="Expect normal helpful conversation.",
)
### alternative stt - cartesia ###
# stt = CartesiaSTTService(api_key=os.getenv("CARTESIA_API_KEY"))
### LLM ###
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
### TTS ###
tts = OpenAITTSService(
api_key=os.getenv("OPENAI_API_KEY"),
voice="ballad",
params=OpenAITTSService.InputParams(instructions="Please speak clearly and at a moderate pace."),
)
### alternative tts - elevenlabs ###
# tts = ElevenLabsTTSService(
# api_key=os.getenv("ELEVENLABS_API_KEY"),
# voice_id=os.getenv("ELEVENLABS_VOICE_ID"),
# model="eleven_turbo_v2_5",
# )
### alternative tts - cartesia ###
# tts = CartesiaTTSService(
# api_key=os.getenv("CARTESIA_API_KEY"),
# voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady
# )
messages = [
{
"role": "system",
"content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio so don't include special characters in your answers. Respond to what the user said in a creative and helpful way.",
},
]
context = LLMContext(messages)
context_aggregator = LLMContextAggregatorPair(context)
### PIPELINE ###
pipeline = Pipeline(
[
transport.input(), # Transport user input
stt,
context_aggregator.user(), # User responses
llm, # LLM
tts, # TTS
transport.output(), # Transport bot output
context_aggregator.assistant(), # Assistant spoken responses
]
)
### TASK ###
turn_detector = TurnDetectorObserver()
task = PipelineTask(
pipeline,
params=PipelineParams(
enable_metrics=True,
enable_usage_metrics=True,
),
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
observers=[turn_detector],
)
turn_detector.set_turn_observer_event_handlers(task.turn_tracking_observer)
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Kick off the conversation.
messages.append({"role": "system", "content": "Please introduce yourself to the user."})
await task.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
logger.info(f"Client disconnected")
await task.cancel()
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
await runner.run(task)
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,11 @@
```bash
uv sync
uv pip install -e '.[cartesia,daily,elevenlabs,local-smart-turn-v3,openai,runner,webrtc]'
```
```bash
python examples/foundational/trace/001-trace.py
```
- open [http://localhost:7860](http://localhost:7860)
- click `connect` button in top right

View File

@@ -0,0 +1,5 @@
OPENAI_API_KEY=...
ELEVENLABS_API_KEY=...
ELEVENLABS_VOICE_ID=...
CARTESIA_API_KEY=...

View File

@@ -0,0 +1,181 @@
import time
from loguru import logger
from pipecat.frames.frames import (
BotStartedSpeakingFrame,
BotStoppedSpeakingFrame,
EndFrame,
FunctionCallResultFrame,
FunctionCallsStartedFrame,
LLMFullResponseEndFrame,
LLMFullResponseStartFrame,
StartFrame,
UserStartedSpeakingFrame,
UserStoppedSpeakingFrame,
)
from pipecat.observers.base_observer import BaseObserver, FramePushed
from pipecat.pipeline.pipeline import Pipeline
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.openai.base_llm import LLMService
from pipecat.transports.base_output import BaseOutputTransport
class TurnDetectorObserver(BaseObserver):
"""Observer ... of turns."""
def __init__(self):
super().__init__()
self._turn_observer = None
self._arrow = ""
self._turn_number = 1
self._endframe_queued = False
def init(self):
"""
Set ...
"""
pass
def set_turn_observer_event_handlers(self, turn_observer):
self._turn_observer = turn_observer
self.set_turn_observer_event_handlers(self._turn_observer)
def get_turn_observer(self):
return self._turn_observer
def set_turn_observer_event_handlers(self, turn_observer):
"""Sets the Turn Observer event handlers `on_turn_started` and `on_turn_ended`.
Args:
turn_observer: The turn tracking observer of the pipeline task
"""
@turn_observer.event_handler("on_turn_started")
async def on_turn_started(observer, turn_number):
self._turn_number = turn_number
current_time = time.time()
logger.info(f"🔄 Turn {turn_number} started")
# 🫆🫆🫆🫆
# code to start conversation turn here
# 🫆🫆🫆🫆
# 🫆🫆🫆🫆
# 🫆🫆🫆🫆
@turn_observer.event_handler("on_turn_ended")
async def on_turn_ended(observer, turn_number, duration, was_interrupted):
current_time = time.time()
if was_interrupted:
logger.info(f"🔄 Turn {turn_number} interrupted after {duration:.2f}s")
else:
logger.info(f"🏁 Turn {turn_number} completed in {duration:.2f}s")
# 🫆🫆🫆🫆
# code to end conversation turn here
# 🫆🫆🫆🫆
# 🫆🫆🫆🫆
# 🫆🫆🫆🫆
########
# everything past here isn't needed, just nice to have logging
########
async def on_push_frame(self, data: FramePushed):
"""Runs when any frame is pushed through pipeline.
Determines based on what type of frame and where it came from
what metrics to update.
Args:
data: the pushed frame
"""
src = data.source
dst = data.destination
frame = data.frame
direction = data.direction
timestamp = data.timestamp
# Convert timestamp to milliseconds for readability
time_sec = timestamp / 1_000_000
# Convert timestamp to seconds for readability
# time_sec = timestamp / 1_000_000_000
# only log downstream frames
if direction == FrameDirection.UPSTREAM:
return
if isinstance(src, Pipeline) or isinstance(dst, Pipeline):
if isinstance(frame, StartFrame):
self._handle_StartFrame(src, dst, frame, time_sec)
elif isinstance(frame, EndFrame):
self._handle_EndFrame(src, dst, frame, time_sec)
if isinstance(src, BaseOutputTransport):
if isinstance(frame, BotStartedSpeakingFrame):
self._handle_BotStartedSpeakingFrame(src, dst, frame, time_sec)
elif isinstance(frame, BotStoppedSpeakingFrame):
self._handle_BotStoppedSpeakingFrame(src, dst, frame, time_sec)
elif isinstance(frame, UserStartedSpeakingFrame):
self._handle_UserStartedSpeakingFrame(src, dst, frame, time_sec)
elif isinstance(frame, UserStoppedSpeakingFrame):
self._handle_UserStoppedSpeakingFrame(src, dst, frame, time_sec)
if isinstance(src, LLMService):
if isinstance(frame, LLMFullResponseStartFrame):
self._handle_LLMFullResponseStartFrame(src, dst, frame, time_sec)
elif isinstance(frame, LLMFullResponseEndFrame):
self._handle_LLMFullResponseEndFrame(src, dst, frame, time_sec)
elif isinstance(frame, FunctionCallsStartedFrame):
self._handle_FunctionCallsStartedFrame(src, dst, frame, time_sec)
elif isinstance(frame, FunctionCallResultFrame):
self._handle_FunctionCallResultFrame(src, dst, frame, time_sec)
# ------------ FRAME HANDLERS ------------
def _handle_StartFrame(self, src, dst, frame, time_sec):
if isinstance(dst, Pipeline):
logger.info(f"🟢🟢🟢 StartFrame: {src} {self._arrow} {dst} at {time_sec:.2f}s")
def _handle_EndFrame(self, src, dst, frame, time_sec):
if isinstance(dst, Pipeline):
logger.info(f"Queueing 🔴🔴🔴 EndFrame: {src} {self._arrow} {dst} at {time_sec:.2f}s")
self._endframe_queued = True
if isinstance(src, Pipeline):
logger.info(f"🔴🔴🔴 EndFrame: {src} {self._arrow} {dst} at {time_sec:.2f}s")
current_time = time.time()
end_state_info = {
"turn_number": self._turn_number,
}
def _handle_BotStartedSpeakingFrame(self, src, dst, frame, time_sec):
logger.info(f"🤖🟢 BotStartedSpeakingFrame: {src} {self._arrow} {dst} at {time_sec:.2f}s")
def _handle_BotStoppedSpeakingFrame(self, src, dst, frame, time_sec):
logger.info(f"🤖🔴 BotStoppedSpeakingFrame: {src} {self._arrow} {dst} at {time_sec:.2f}s")
def _handle_LLMFullResponseStartFrame(self, src, dst, frame, time_sec):
logger.info(f"🧠🟢 LLMFullResponseStartFrame: {src} {self._arrow} {dst} at {time_sec:.2f}s")
def _handle_LLMFullResponseEndFrame(self, src, dst, frame, time_sec):
logger.info(f"🧠🔴 LLMFullResponseEndFrame: {src} {self._arrow} {dst} at {time_sec:.2f}s")
def _handle_UserStartedSpeakingFrame(self, src, dst, frame, time_sec):
logger.info(f"🙂🟢 UserStartedSpeakingFrame: {src} {self._arrow} {dst} at {time_sec:.2f}s")
def _handle_UserStoppedSpeakingFrame(self, src, dst, frame, time_sec):
logger.info(f"🙂🔴 UserStoppedSpeakingFrame: {src} {self._arrow} {dst} at {time_sec:.2f}s")
def _handle_FunctionCallsStartedFrame(self, src, dst, frame, time_sec):
logger.info(
f"📐🟢 {frame.function_calls[0].function_name} FunctionCallsStartedFrame: {src} {self._arrow} {dst} at {time_sec:.2f}s"
)
def _handle_FunctionCallResultFrame(self, src, dst, frame, time_sec):
logger.info(
f"📐🔴 {frame.function_name} FunctionCallResultFrame: {src} {self._arrow} {dst} at {time_sec:.2f}s"
)