Compare commits

...

12 Commits

Author SHA1 Message Date
Aleix Conchillo Flaqué
dcf7e454f6 services(tts): don't wait for BotStoppedSpeakingFrame to resume input queue 2024-12-02 19:00:47 -08:00
Aleix Conchillo Flaqué
bec46a87ae Merge pull request #766 from Allenmylath/patch-20
Update requirements.txt
2024-12-02 10:32:36 -08:00
allenmylath
acbecf1c4c Update requirements.txt
daily is not used here.transport is fastapi websocket.
2024-12-02 21:36:29 +05:30
Mark Backman
6095fd342e Merge pull request #763 from Allenmylath/patch-19
Update README.md
2024-12-02 09:30:36 -05:00
allenmylath
23316fbcf9 Update README.md 2024-12-02 13:35:44 +05:30
James Hush
5e22ef251d fix: add logging and error handling for issue #721 (#755) 2024-11-29 13:06:45 +08:00
Mark Backman
c5324df807 Merge pull request #752 from pipecat-ai/mb/google-context-message-conversion
Use Google Gemini message format when adding message to the LLM context
2024-11-27 14:13:17 -05:00
Mark Backman
3c19a7ae3d Use Google Gemini message format when adding message to the LLM context 2024-11-27 12:46:51 -05:00
Mark Backman
98c0a6e047 Merge pull request #749 from pipecat-ai/mb/pipecat-flows-standalone
Make Pipecat Flows an independent package
2024-11-25 17:09:11 -05:00
Mark Backman
f599e160de Make Pipecat Flows an independent package 2024-11-25 13:42:08 -05:00
Mark Backman
11c5d822f9 Merge pull request #746 from pipecat-ai/mb/update-flows
Bumping pipecat-ai-flows version
2024-11-22 11:25:03 -05:00
Mark Backman
c3e22f0931 Bumping pipecat-ai-flows version 2024-11-22 11:21:40 -05:00
11 changed files with 75 additions and 296 deletions

View File

@@ -12,12 +12,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added a new RTVI message called `disconnect-bot`, which when handled pushes
an `EndFrame` to trigger the pipeline to stop.
- Added support for the new Pipecat Flows package (pipecat-ai-flows). Learn
more at: https://github.com/pipecat-ai/pipecat-flows.
- Added foundational example `25-conversation-flow.py` showing how to use
Pipecat Flows.
### Changed
- Expanded the transcriptions.language module to support a superset of
@@ -26,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updated STT and TTS services with language options that match the supported
languages for each service.
### Fixed
- Fixed Google Gemini message handling to properly convert appended messages to Gemini's required format
## [0.0.49] - 2024-11-17
### Added

View File

@@ -13,6 +13,7 @@ Pipecat is an open source Python framework for building voice and multimodal con
- **Multimodal Apps**: Combine voice, video, images, and text
- **Creative Tools**: [Story-telling experiences](https://storytelling-chatbot.fly.dev/) and social companions
- **Business Solutions**: [Customer intake flows](https://www.youtube.com/watch?v=lDevgsp9vn0) and support bots
- **Complex conversational flows**: [Refer to Pipecat Flows](https://github.com/pipecat-ai/pipecat-flows) to learn more
## See it in action
@@ -32,6 +33,8 @@ Pipecat is an open source Python framework for building voice and multimodal con
- **Real-time Processing**: Frame-based pipeline architecture for fluid interactions
- **Production Ready**: Enterprise-grade WebRTC and Websocket support
💡 Looking to build structured conversations? Check out [Pipecat Flows](https://github.com/pipecat-ai/pipecat-flows) for managing complex conversational states and transitions.
## Getting started
You can get started with Pipecat running on your local machine, then move your agent processes to the cloud when youre ready. You can also add a 📞 telephone number, 🖼️ image output, 📺 video input, use different LLMs, and more.

View File

@@ -42,6 +42,7 @@ Next, follow the steps in the README for each demo.
| [Dialin Chatbot](dialin-chatbot) | A chatbot that connects to an incoming phone call from Daily or Twilio. | Deepgram, ElevenLabs, OpenAI, Daily, Twilio |
| [Twilio Chatbot](twilio-chatbot) | A chatbot that connects to an incoming phone call from Twilio. | Deepgram, ElevenLabs, OpenAI, Daily, Twilio |
| [studypal](studypal) | A chatbot to have a conversation about any article on the web | |
| [WebSocket Chatbot Server](websocket-server) | A real-time websocket server that handles audio streaming and bot interactions with speech-to-text and text-to-speech capabilities | `python-websockets`, `openai`, `deepgram`, `silero-tts`, `numpy` |
> [!IMPORTANT]
> These example projects use Daily as a WebRTC transport and can be joined using their hosted Prebuilt UI.

View File

@@ -1,266 +0,0 @@
#
# Copyright (c) 2024, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import asyncio
import os
import sys
import aiohttp
from dotenv import load_dotenv
from loguru import logger
from pipecat_flows import FlowManager
from runner import configure
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.services.deepgram import DeepgramSTTService, DeepgramTTSService
from pipecat.services.openai import OpenAILLMService
from pipecat.transports.services.daily import DailyParams, DailyTransport
load_dotenv(override=True)
logger.remove(0)
logger.add(sys.stderr, level="DEBUG")
# Flow Configuration
#
# This configuration defines a simple food ordering system with the following states:
#
# 1. start
# - Initial state where user chooses between pizza or sushi
# - Functions: choose_pizza, choose_sushi
# - Transitions to: choose_pizza or choose_sushi
#
# 2. choose_pizza
# - Handles pizza size selection and order confirmation
# - Functions:
# * select_pizza_size (terminal function, can be called multiple times)
# * end (transitions to end node after order confirmation)
# - Pre-action: Immediate TTS acknowledgment
#
# 3. choose_sushi
# - Handles sushi roll count selection and order confirmation
# - Functions:
# * select_roll_count (terminal function, can be called multiple times)
# * end (transitions to end node after order confirmation)
# - Pre-action: Immediate TTS acknowledgment
#
# 4. end
# - Final state that closes the conversation
# - No functions available
# - Pre-action: Farewell message
# - Post-action: Ends conversation
flow_config = {
"initial_node": "start",
"nodes": {
"start": {
"messages": [
{
"role": "system",
"content": "You are an order-taking assistant. You must ALWAYS use one of the available functions to progress the conversation. For this step, ask the user if they want pizza or sushi, and wait for them to use a function to choose. Start off by greeting them. Be friendly and casual; you're taking an order for food over the phone.",
}
],
"functions": [
{
"type": "function",
"function": {
"name": "choose_pizza",
"description": "User wants to order pizza",
"parameters": {"type": "object", "properties": {}},
},
},
{
"type": "function",
"function": {
"name": "choose_sushi",
"description": "User wants to order sushi",
"parameters": {"type": "object", "properties": {}},
},
},
],
},
"choose_pizza": {
"messages": [
{
"role": "system",
"content": """You are handling a pizza order. Use the available functions:
- Use select_pizza_size when the user specifies a size (can be used multiple times if they change their mind)
- Use the end function ONLY when the user confirms they are done with their order
After each size selection, confirm the selection and ask if they want to change it or complete their order.
Only use the end function after the user confirms they are satisfied with their order.
Start off by acknowledging the user's choice. Once they've chosen a size, ask if they'd like anything else.
Remember to be friendly and casual.""",
}
],
"functions": [
{
"type": "function",
"function": {
"name": "select_pizza_size",
"description": "Record the selected pizza size",
"parameters": {
"type": "object",
"properties": {
"size": {
"type": "string",
"enum": ["small", "medium", "large"],
"description": "Size of the pizza",
}
},
"required": ["size"],
},
},
},
{
"type": "function",
"function": {
"name": "end",
"description": "Complete the order (use only after user confirms)",
"parameters": {"type": "object", "properties": {}},
},
},
],
"pre_actions": [
{"type": "tts_say", "text": "Ok, let me help you with your pizza order..."}
],
},
"choose_sushi": {
"messages": [
{
"role": "system",
"content": """You are handling a sushi order. Use the available functions:
- Use select_roll_count when the user specifies how many rolls (can be used multiple times if they change their mind)
- Use the end function ONLY when the user confirms they are done with their order
After each roll count selection, confirm the count and ask if they want to change it or complete their order.
Only use the end function after the user confirms they are satisfied with their order.
Start off by acknowledging the user's choice. Once they've chosen a size, ask if they'd like anything else.
Remember to be friendly and casual.""",
}
],
"functions": [
{
"type": "function",
"function": {
"name": "select_roll_count",
"description": "Record the number of sushi rolls",
"parameters": {
"type": "object",
"properties": {
"count": {
"type": "integer",
"minimum": 1,
"maximum": 10,
"description": "Number of rolls to order",
}
},
"required": ["count"],
},
},
},
{
"type": "function",
"function": {
"name": "end",
"description": "Complete the order (use only after user confirms)",
"parameters": {"type": "object", "properties": {}},
},
},
],
"pre_actions": [
{"type": "tts_say", "text": "Ok, let me help you with your sushi order..."}
],
},
"end": {
"messages": [
{
"role": "system",
"content": "The order is complete. Thank the user and end the conversation.",
}
],
"functions": [],
"pre_actions": [{"type": "tts_say", "text": "Thank you for your order! Goodbye!"}],
"post_actions": [{"type": "end_conversation"}],
},
},
}
async def main():
async with aiohttp.ClientSession() as session:
(room_url, _) = await configure(session)
transport = DailyTransport(
room_url,
None,
"Respond bot",
DailyParams(
audio_out_enabled=True,
vad_enabled=True,
vad_analyzer=SileroVADAnalyzer(),
vad_audio_passthrough=True,
),
)
stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"))
tts = DeepgramTTSService(api_key=os.getenv("DEEPGRAM_API_KEY"), voice="aura-helios-en")
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4")
# Get initial tools from the first node
initial_tools = flow_config["nodes"]["start"]["functions"]
# Create initial context
messages = [
{
"role": "system",
"content": "You are an order-taking assistant. You must ALWAYS use the available functions to progress the conversation. Never assume an order is complete without the proper function calls. Your responses will be converted to audio so avoid special characters.",
}
]
context = OpenAILLMContext(messages, initial_tools)
context_aggregator = llm.create_context_aggregator(context)
pipeline = Pipeline(
[
transport.input(), # Transport user input
stt, # STT
context_aggregator.user(), # User responses
llm, # LLM
tts, # TTS
transport.output(), # Transport bot output
context_aggregator.assistant(), # Assistant spoken responses
]
)
task = PipelineTask(pipeline, PipelineParams(allow_interruptions=True))
# Initialize flow manager
flow_manager = FlowManager(flow_config, task, tts)
# Register functions with LLM service
await flow_manager.register_functions(llm)
@transport.event_handler("on_first_participant_joined")
async def on_first_participant_joined(transport, participant):
await transport.capture_participant_transcription(participant["id"])
# Initialize the flow processor
await flow_manager.initialize(messages)
# Kick off the conversation using the context aggregator
await task.queue_frames([context_aggregator.user().get_context_frame()])
runner = PipelineRunner()
await runner.run(task)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,4 +1,4 @@
pipecat-ai[daily,cartesia,openai,silero,deepgram]
pipecat-ai[cartesia,openai,silero,deepgram]
fastapi
uvicorn
python-dotenv

View File

@@ -51,7 +51,6 @@ gladia = [ "websockets~=13.1" ]
google = [ "google-generativeai~=0.8.3", "google-cloud-texttospeech~=2.17.2" ]
gstreamer = [ "pygobject~=3.48.2" ]
fireworks = [ "openai~=1.37.2" ]
flows = [ "pipecat-ai-flows~=0.0.1" ]
krisp = [ "pipecat-ai-krisp~=0.3.0" ]
langchain = [ "langchain~=0.2.14", "langchain-community~=0.2.12", "langchain-openai~=0.1.20" ]
livekit = [ "livekit~=0.17.5", "livekit-api~=0.7.1", "tenacity~=8.5.0" ]

View File

@@ -6,10 +6,11 @@
import asyncio
import inspect
from enum import Enum
from typing import Awaitable, Callable, Optional
from loguru import logger
from pipecat.clocks.base_clock import BaseClock
from pipecat.frames.frames import (
EndFrame,
@@ -24,8 +25,6 @@ from pipecat.metrics.metrics import LLMTokenUsage, MetricsData
from pipecat.processors.metrics.frame_processor_metrics import FrameProcessorMetrics
from pipecat.utils.utils import obj_count, obj_id
from loguru import logger
class FrameDirection(Enum):
DOWNSTREAM = 1
@@ -220,11 +219,16 @@ class FrameProcessor:
#
async def _start_interruption(self):
# Cancel the push frame task. This will stop pushing frames downstream.
await self.__cancel_push_task()
try:
# Cancel the push frame task. This will stop pushing frames downstream.
await self.__cancel_push_task()
# Cancel the input task. This will stop processing queued frames.
await self.__cancel_input_task()
# Cancel the input task. This will stop processing queued frames.
await self.__cancel_input_task()
except Exception as e:
logger.exception(f"Uncaught exception in {self}: {e}")
await self.push_error(ErrorFrame(str(e)))
raise
# Create a new input queue and task.
self.__create_input_task()
@@ -281,7 +285,11 @@ class FrameProcessor:
self.__input_queue.task_done()
except asyncio.CancelledError:
logger.trace(f"Cancelled input task in {self}")
break
except Exception as e:
logger.exception(f"Uncaught exception in {self}: {e}")
await self.push_error(ErrorFrame(str(e)))
def __create_push_task(self):
self.__push_queue = asyncio.Queue()
@@ -300,7 +308,11 @@ class FrameProcessor:
running = not isinstance(frame, EndFrame)
self.__push_queue.task_done()
except asyncio.CancelledError:
logger.trace(f"Cancelled push task in {self}")
break
except Exception as e:
logger.exception(f"Uncaught exception in {self}: {e}")
await self.push_error(ErrorFrame(str(e)))
async def _call_event_handler(self, event_name: str, *args, **kwargs):
try:

View File

@@ -14,7 +14,6 @@ from loguru import logger
from pydantic.main import BaseModel
from pipecat.frames.frames import (
BotStoppedSpeakingFrame,
CancelFrame,
EndFrame,
ErrorFrame,
@@ -259,18 +258,25 @@ class CartesiaTTSService(WordTTSService):
except Exception as e:
logger.error(f"{self} exception: {e}")
async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM):
await super().push_frame(frame, direction)
# We generate LLMFullResponseEndFrame after we have received all the
# audio from the service which means we can resume processing frames.
if isinstance(frame, LLMFullResponseEndFrame):
await self.resume_processing_frames()
async def process_frame(self, frame: Frame, direction: FrameDirection):
await super().process_frame(frame, direction)
# If we received a TTSSpeakFrame and the LLM response included text (it
# If we received a TTSSpeakFrame or the LLM response included text (it
# might be that it's only a function calling response) we pause
# processing more frames until we receive a BotStoppedSpeakingFrame.
# processing more frames until we have generated LLMFullResponseEndFrame
# (see push_frame()).
if isinstance(frame, TTSSpeakFrame):
await self.pause_processing_frames()
elif isinstance(frame, LLMFullResponseEndFrame) and self._context_id:
await self.pause_processing_frames()
elif isinstance(frame, BotStoppedSpeakingFrame):
await self.resume_processing_frames()
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
logger.debug(f"Generating TTS: [{text}]")

View File

@@ -13,7 +13,6 @@ from loguru import logger
from pydantic import BaseModel, model_validator
from pipecat.frames.frames import (
BotStoppedSpeakingFrame,
CancelFrame,
EndFrame,
Frame,
@@ -262,23 +261,28 @@ class ElevenLabsTTSService(WordTTSService):
async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM):
await super().push_frame(frame, direction)
if isinstance(frame, (TTSStoppedFrame, StartInterruptionFrame)):
self._started = False
if isinstance(frame, TTSStoppedFrame):
await self.add_word_timestamps([("LLMFullResponseEndFrame", 0), ("Reset", 0)])
# We generate LLMFullResponseEndFrame after we have received all the
# audio from the service which means we can resume processing frames.
if isinstance(frame, LLMFullResponseEndFrame):
await self.resume_processing_frames()
async def process_frame(self, frame: Frame, direction: FrameDirection):
await super().process_frame(frame, direction)
# If we received a TTSSpeakFrame and the LLM response included text (it
# If we received a TTSSpeakFrame or the LLM response included text (it
# might be that it's only a function calling response) we pause
# processing more frames until we receive a BotStoppedSpeakingFrame.
# processing more frames until we have generated LLMFullResponseEndFrame
# (see push_frame()).
if isinstance(frame, TTSSpeakFrame):
await self.pause_processing_frames()
elif isinstance(frame, LLMFullResponseEndFrame) and self._started:
await self.pause_processing_frames()
elif isinstance(frame, BotStoppedSpeakingFrame):
await self.resume_processing_frames()
async def _connect(self):
try:

View File

@@ -332,6 +332,22 @@ class GoogleLLMContext(OpenAILLMContext):
self._messages[:] = messages
self._restructure_from_openai_messages()
def add_messages(self, messages: List):
# Convert each message individually
converted_messages = []
for msg in messages:
if isinstance(msg, glm.Content):
# Already in Gemini format
converted_messages.append(msg)
else:
# Convert from standard format to Gemini format
converted = self.from_standard_message(msg)
if converted is not None:
converted_messages.append(converted)
# Add the converted messages to our existing messages
self._messages.extend(converted_messages)
def get_messages_for_logging(self):
msgs = []
for message in self.messages:

View File

@@ -17,7 +17,6 @@ from loguru import logger
from pydantic.main import BaseModel
from pipecat.frames.frames import (
BotStoppedSpeakingFrame,
CancelFrame,
EndFrame,
ErrorFrame,
@@ -235,18 +234,25 @@ class PlayHTTTSService(TTSService):
except Exception as e:
logger.error(f"{self} exception in receive task: {e}")
async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM):
await super().push_frame(frame, direction)
# We generate LLMFullResponseEndFrame after we have received all the
# audio from the service which means we can resume processing frames.
if isinstance(frame, LLMFullResponseEndFrame):
await self.resume_processing_frames()
async def process_frame(self, frame: Frame, direction: FrameDirection):
await super().process_frame(frame, direction)
# If we received a TTSSpeakFrame and the LLM response included text (it
# If we received a TTSSpeakFrame or the LLM response included text (it
# might be that it's only a function calling response) we pause
# processing more frames until we receive a BotStoppedSpeakingFrame.
# processing more frames until we have generated LLMFullResponseEndFrame
# (see push_frame()).
if isinstance(frame, TTSSpeakFrame):
await self.pause_processing_frames()
elif isinstance(frame, LLMFullResponseEndFrame) and self._request_id:
await self.pause_processing_frames()
elif isinstance(frame, BotStoppedSpeakingFrame):
await self.resume_processing_frames()
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
logger.debug(f"Generating TTS: [{text}]")