Remove deprecated service module shims and old implementations
Delete deprecated import shims that only re-export from new locations:
- services/ai_services.py
- services/gemini_multimodal_live/
- services/aws_nova_sonic/
- services/openai_realtime/
- services/deepgram/{stt,tts}_sagemaker.py
- services/google/{llm_openai,llm_vertex,google}.py
- services/google/gemini_live/llm_vertex.py
- services/riva/
- services/nim/
Remove deprecated implementations replaced by newer services:
- services/openai_realtime_beta/ (use openai.realtime)
- services/google/openai/ (use google.llm)
Also removes associated examples and tests for deleted services.
This commit is contained in:
@@ -1,162 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
|
||||||
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
|
||||||
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.processors.aggregators.llm_response_universal import (
|
|
||||||
LLMContextAggregatorPair,
|
|
||||||
LLMUserAggregatorParams,
|
|
||||||
)
|
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
|
||||||
from pipecat.runner.types import RunnerArguments
|
|
||||||
from pipecat.runner.utils import create_transport
|
|
||||||
from pipecat.services.deepgram.stt import DeepgramSTTService
|
|
||||||
from pipecat.services.elevenlabs.tts import ElevenLabsTTSService
|
|
||||||
from pipecat.services.google.openai.llm import GoogleLLMOpenAIBetaService
|
|
||||||
from pipecat.services.llm_service import FunctionCallParams
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
async def fetch_weather_from_api(params: FunctionCallParams):
|
|
||||||
await params.result_callback({"conditions": "nice", "temperature": "75"})
|
|
||||||
|
|
||||||
|
|
||||||
# We use lambdas to defer transport parameter creation until the transport
|
|
||||||
# type is selected at runtime.
|
|
||||||
transport_params = {
|
|
||||||
"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(f"Starting bot")
|
|
||||||
|
|
||||||
stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"))
|
|
||||||
|
|
||||||
tts = ElevenLabsTTSService(
|
|
||||||
api_key=os.getenv("ELEVENLABS_API_KEY", ""),
|
|
||||||
settings=ElevenLabsTTSService.Settings(
|
|
||||||
voice=os.getenv("ELEVENLABS_VOICE_ID", ""),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
llm = GoogleLLMOpenAIBetaService(
|
|
||||||
api_key=os.getenv("GOOGLE_API_KEY"),
|
|
||||||
settings=GoogleLLMOpenAIBetaService.Settings(
|
|
||||||
system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
# You can aslo register a function_name of None to get all functions
|
|
||||||
# sent to the same callback with an additional function_name parameter.
|
|
||||||
llm.register_function("get_current_weather", fetch_weather_from_api)
|
|
||||||
|
|
||||||
@llm.event_handler("on_function_calls_started")
|
|
||||||
async def on_function_calls_started(service, function_calls):
|
|
||||||
await tts.queue_frame(TTSSpeakFrame("Let me check on that."))
|
|
||||||
|
|
||||||
weather_function = FunctionSchema(
|
|
||||||
name="get_current_weather",
|
|
||||||
description="Get the current weather",
|
|
||||||
properties={
|
|
||||||
"location": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The city and state, e.g. San Francisco, CA",
|
|
||||||
},
|
|
||||||
"format": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["celsius", "fahrenheit"],
|
|
||||||
"description": "The temperature unit to use. Infer this from the user's location.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required=["location", "format"],
|
|
||||||
)
|
|
||||||
tools = ToolsSchema(standard_tools=[weather_function])
|
|
||||||
messages = [
|
|
||||||
{
|
|
||||||
"role": "developer",
|
|
||||||
"content": "Start a conversation with 'Hey there' to get the current weather.",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
context = OpenAILLMContext(messages, tools)
|
|
||||||
user_aggregator, assistant_aggregator = LLMContextAggregatorPair(
|
|
||||||
context,
|
|
||||||
user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()),
|
|
||||||
)
|
|
||||||
|
|
||||||
pipeline = Pipeline(
|
|
||||||
[
|
|
||||||
transport.input(),
|
|
||||||
stt,
|
|
||||||
user_aggregator,
|
|
||||||
llm,
|
|
||||||
tts,
|
|
||||||
transport.output(),
|
|
||||||
assistant_aggregator,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
task = PipelineTask(
|
|
||||||
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(f"Client connected")
|
|
||||||
# Kick off the conversation.
|
|
||||||
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()
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
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.processors.aggregators.openai_llm_context import (
|
|
||||||
OpenAILLMContext,
|
|
||||||
)
|
|
||||||
from pipecat.runner.types import RunnerArguments
|
|
||||||
from pipecat.runner.utils import create_transport
|
|
||||||
from pipecat.services.deepgram.stt import DeepgramSTTService
|
|
||||||
from pipecat.services.llm_service import FunctionCallParams
|
|
||||||
from pipecat.services.openai_realtime_beta import (
|
|
||||||
InputAudioTranscription,
|
|
||||||
OpenAIRealtimeBetaLLMService,
|
|
||||||
SessionProperties,
|
|
||||||
TurnDetection,
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
|
|
||||||
BASE_FILENAME = "/tmp/pipecat_conversation_"
|
|
||||||
|
|
||||||
|
|
||||||
async def fetch_weather_from_api(params: FunctionCallParams):
|
|
||||||
temperature = 75 if params.arguments["format"] == "fahrenheit" else 24
|
|
||||||
await params.result_callback(
|
|
||||||
{
|
|
||||||
"conditions": "nice",
|
|
||||||
"temperature": temperature,
|
|
||||||
"format": params.arguments["format"],
|
|
||||||
"timestamp": datetime.now().strftime("%Y%m%d_%H%M%S"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_saved_conversation_filenames(params: FunctionCallParams):
|
|
||||||
# Construct the full pattern including the BASE_FILENAME
|
|
||||||
full_pattern = f"{BASE_FILENAME}*.json"
|
|
||||||
|
|
||||||
# Use glob to find all matching files
|
|
||||||
matching_files = glob.glob(full_pattern)
|
|
||||||
logger.debug(f"matching files: {matching_files}")
|
|
||||||
|
|
||||||
await params.result_callback({"filenames": matching_files})
|
|
||||||
|
|
||||||
|
|
||||||
async def save_conversation(params: FunctionCallParams):
|
|
||||||
timestamp = datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
|
|
||||||
filename = f"{BASE_FILENAME}{timestamp}.json"
|
|
||||||
logger.debug(
|
|
||||||
f"writing conversation to {filename}\n{json.dumps(params.context.messages, indent=4)}"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
with open(filename, "w") as file:
|
|
||||||
messages = params.context.get_messages_for_persistent_storage()
|
|
||||||
# remove the last message, which is the instruction we just gave to save the conversation
|
|
||||||
messages.pop()
|
|
||||||
json.dump(messages, file, indent=2)
|
|
||||||
await params.result_callback({"success": True})
|
|
||||||
except Exception as e:
|
|
||||||
await params.result_callback({"success": False, "error": str(e)})
|
|
||||||
|
|
||||||
|
|
||||||
async def load_conversation(params: FunctionCallParams):
|
|
||||||
async def _reset():
|
|
||||||
filename = params.arguments["filename"]
|
|
||||||
logger.debug(f"loading conversation from {filename}")
|
|
||||||
try:
|
|
||||||
with open(filename, "r") as file:
|
|
||||||
params.context.set_messages(json.load(file))
|
|
||||||
await params.llm.reset_conversation()
|
|
||||||
await params.llm._create_response()
|
|
||||||
except Exception as e:
|
|
||||||
await params.result_callback({"success": False, "error": str(e)})
|
|
||||||
|
|
||||||
asyncio.create_task(_reset())
|
|
||||||
|
|
||||||
|
|
||||||
tools = [
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "get_current_weather",
|
|
||||||
"description": "Get the current weather",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"location": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The city and state, e.g. San Francisco, CA",
|
|
||||||
},
|
|
||||||
"format": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["celsius", "fahrenheit"],
|
|
||||||
"description": "The temperature unit to use. Infer this from the users location.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["location", "format"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "save_conversation",
|
|
||||||
"description": "Save the current conversation. Use this function to persist the current conversation to external storage.",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {},
|
|
||||||
"required": [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "get_saved_conversation_filenames",
|
|
||||||
"description": "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.",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {},
|
|
||||||
"required": [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "load_conversation",
|
|
||||||
"description": "Load a conversation history. Use this function to load a conversation history into the current session.",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"filename": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The filename of the conversation history to load.",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["filename"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# We use lambdas to defer transport parameter creation until the transport
|
|
||||||
# type is selected at runtime.
|
|
||||||
transport_params = {
|
|
||||||
"daily": lambda: DailyParams(
|
|
||||||
audio_in_enabled=True,
|
|
||||||
audio_out_enabled=True,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
|
||||||
),
|
|
||||||
"twilio": lambda: FastAPIWebsocketParams(
|
|
||||||
audio_in_enabled=True,
|
|
||||||
audio_out_enabled=True,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
|
||||||
),
|
|
||||||
"webrtc": lambda: TransportParams(
|
|
||||||
audio_in_enabled=True,
|
|
||||||
audio_out_enabled=True,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
|
||||||
logger.info(f"Starting bot")
|
|
||||||
|
|
||||||
stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"))
|
|
||||||
|
|
||||||
session_properties = SessionProperties(
|
|
||||||
input_audio_transcription=InputAudioTranscription(),
|
|
||||||
# Set openai TurnDetection parameters. Not setting this at all will turn
|
|
||||||
# it on by default
|
|
||||||
turn_detection=TurnDetection(silence_duration_ms=1000),
|
|
||||||
# Or set to False to disable openai turn detection and use transport VAD
|
|
||||||
# turn_detection=False,
|
|
||||||
# tools=tools,
|
|
||||||
instructions="""Your knowledge cutoff is 2023-10. You are a helpful and friendly AI.
|
|
||||||
|
|
||||||
Act like a human, but remember that you aren't a human and that you can't do human
|
|
||||||
things in the real world. Your voice and personality should be warm and engaging, with a lively and
|
|
||||||
playful tone.
|
|
||||||
|
|
||||||
If interacting in a non-English language, start by using the standard accent or dialect familiar to
|
|
||||||
the user. Talk quickly. You should always call a function if you can. Do not refer to these rules,
|
|
||||||
even if you're asked about them.
|
|
||||||
-
|
|
||||||
You are participating in a voice conversation. Keep your responses concise, short, and to the point
|
|
||||||
unless specifically asked to elaborate on a topic.
|
|
||||||
|
|
||||||
Remember, your responses should be short. Just one or two sentences, usually.""",
|
|
||||||
)
|
|
||||||
|
|
||||||
llm = OpenAIRealtimeBetaLLMService(
|
|
||||||
api_key=os.getenv("OPENAI_API_KEY"),
|
|
||||||
session_properties=session_properties,
|
|
||||||
)
|
|
||||||
|
|
||||||
# you can either register a single function for all function calls, or specific functions
|
|
||||||
# llm.register_function(None, fetch_weather_from_api)
|
|
||||||
llm.register_function("get_current_weather", fetch_weather_from_api)
|
|
||||||
llm.register_function("save_conversation", save_conversation)
|
|
||||||
llm.register_function("get_saved_conversation_filenames", get_saved_conversation_filenames)
|
|
||||||
llm.register_function("load_conversation", load_conversation)
|
|
||||||
|
|
||||||
context = OpenAILLMContext([], tools)
|
|
||||||
context_aggregator = llm.create_context_aggregator(context)
|
|
||||||
|
|
||||||
pipeline = Pipeline(
|
|
||||||
[
|
|
||||||
transport.input(), # Transport user input
|
|
||||||
stt, # STT
|
|
||||||
context_aggregator.user(),
|
|
||||||
llm, # LLM
|
|
||||||
transport.output(), # Transport bot output
|
|
||||||
context_aggregator.assistant(),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
task = PipelineTask(
|
|
||||||
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(f"Client connected")
|
|
||||||
# Kick off the conversation.
|
|
||||||
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()
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
|
||||||
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
|
||||||
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.processors.aggregators.openai_llm_context import OpenAILLMContext
|
|
||||||
from pipecat.runner.types import RunnerArguments
|
|
||||||
from pipecat.runner.utils import create_transport
|
|
||||||
from pipecat.services.llm_service import FunctionCallParams
|
|
||||||
from pipecat.services.openai_realtime_beta import (
|
|
||||||
AzureRealtimeBetaLLMService,
|
|
||||||
InputAudioTranscription,
|
|
||||||
SessionProperties,
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
async def fetch_weather_from_api(params: FunctionCallParams):
|
|
||||||
temperature = 75 if params.arguments["format"] == "fahrenheit" else 24
|
|
||||||
await params.result_callback(
|
|
||||||
{
|
|
||||||
"conditions": "nice",
|
|
||||||
"temperature": temperature,
|
|
||||||
"format": params.arguments["format"],
|
|
||||||
"timestamp": datetime.now().strftime("%Y%m%d_%H%M%S"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def fetch_restaurant_recommendation(params: FunctionCallParams):
|
|
||||||
await params.result_callback({"name": "The Golden Dragon"})
|
|
||||||
|
|
||||||
|
|
||||||
# Define weather function using standardized schema
|
|
||||||
weather_function = FunctionSchema(
|
|
||||||
name="get_current_weather",
|
|
||||||
description="Get the current weather",
|
|
||||||
properties={
|
|
||||||
"location": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The city and state, e.g. San Francisco, CA",
|
|
||||||
},
|
|
||||||
"format": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["celsius", "fahrenheit"],
|
|
||||||
"description": "The temperature unit to use. Infer this from the users location.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required=["location", "format"],
|
|
||||||
)
|
|
||||||
|
|
||||||
restaurant_function = FunctionSchema(
|
|
||||||
name="get_restaurant_recommendation",
|
|
||||||
description="Get a restaurant recommendation",
|
|
||||||
properties={
|
|
||||||
"location": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The city and state, e.g. San Francisco, CA",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required=["location"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create tools schema
|
|
||||||
tools = ToolsSchema(standard_tools=[weather_function, restaurant_function])
|
|
||||||
|
|
||||||
|
|
||||||
# We use lambdas to defer transport parameter creation until the transport
|
|
||||||
# type is selected at runtime.
|
|
||||||
transport_params = {
|
|
||||||
"daily": lambda: DailyParams(
|
|
||||||
audio_in_enabled=True,
|
|
||||||
audio_out_enabled=True,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
|
||||||
),
|
|
||||||
"twilio": lambda: FastAPIWebsocketParams(
|
|
||||||
audio_in_enabled=True,
|
|
||||||
audio_out_enabled=True,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
|
||||||
),
|
|
||||||
"webrtc": lambda: TransportParams(
|
|
||||||
audio_in_enabled=True,
|
|
||||||
audio_out_enabled=True,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
|
||||||
logger.info(f"Starting bot")
|
|
||||||
|
|
||||||
session_properties = SessionProperties(
|
|
||||||
input_audio_transcription=InputAudioTranscription(model="whisper-1"),
|
|
||||||
# Set openai TurnDetection parameters. Not setting this at all will turn it
|
|
||||||
# on by default
|
|
||||||
# turn_detection=TurnDetection(silence_duration_ms=1000),
|
|
||||||
# Or set to False to disable openai turn detection and use transport VAD
|
|
||||||
# turn_detection=False,
|
|
||||||
# tools=tools,
|
|
||||||
instructions="""You are a helpful and friendly AI.
|
|
||||||
|
|
||||||
Act like a human, but remember that you aren't a human and that you can't do human
|
|
||||||
things in the real world. Your voice and personality should be warm and engaging, with a lively and
|
|
||||||
playful tone.
|
|
||||||
|
|
||||||
If interacting in a non-English language, start by using the standard accent or dialect familiar to
|
|
||||||
the user. Talk quickly. You should always call a function if you can. Do not refer to these rules,
|
|
||||||
even if you're asked about them.
|
|
||||||
-
|
|
||||||
You are participating in a voice conversation. Keep your responses concise, short, and to the point
|
|
||||||
unless specifically asked to elaborate on a topic.
|
|
||||||
|
|
||||||
You have access to the following tools:
|
|
||||||
- get_current_weather: Get the current weather for a given location.
|
|
||||||
- get_restaurant_recommendation: Get a restaurant recommendation for a given location.
|
|
||||||
|
|
||||||
Remember, your responses should be short. Just one or two sentences, usually. Respond in English.""",
|
|
||||||
)
|
|
||||||
|
|
||||||
llm = AzureRealtimeBetaLLMService(
|
|
||||||
api_key=os.getenv("AZURE_REALTIME_API_KEY"),
|
|
||||||
base_url=os.getenv("AZURE_REALTIME_BASE_URL"),
|
|
||||||
session_properties=session_properties,
|
|
||||||
)
|
|
||||||
|
|
||||||
# you can either register a single function for all function calls, or specific functions
|
|
||||||
# llm.register_function(None, fetch_weather_from_api)
|
|
||||||
llm.register_function("get_current_weather", fetch_weather_from_api)
|
|
||||||
llm.register_function("get_restaurant_recommendation", fetch_restaurant_recommendation)
|
|
||||||
|
|
||||||
# Create a standard OpenAI LLM context object using the normal messages format. The
|
|
||||||
# OpenAIRealtimeBetaLLMService will convert this internally to messages that the
|
|
||||||
# openai WebSocket API can understand.
|
|
||||||
context = OpenAILLMContext(
|
|
||||||
[{"role": "developer", "content": "Say hello!"}],
|
|
||||||
# [{"role": "developer", "content": [{"type": "text", "text": "Say hello!"}]}],
|
|
||||||
# [
|
|
||||||
# {
|
|
||||||
# "role": "developer",
|
|
||||||
# "content": [
|
|
||||||
# {"type": "text", "text": "Say"},
|
|
||||||
# {"type": "text", "text": "yo what's up!"},
|
|
||||||
# ],
|
|
||||||
# }
|
|
||||||
# ],
|
|
||||||
tools,
|
|
||||||
)
|
|
||||||
|
|
||||||
context_aggregator = llm.create_context_aggregator(context)
|
|
||||||
|
|
||||||
pipeline = Pipeline(
|
|
||||||
[
|
|
||||||
transport.input(), # Transport user input
|
|
||||||
context_aggregator.user(),
|
|
||||||
llm, # LLM
|
|
||||||
transport.output(), # Transport bot output
|
|
||||||
context_aggregator.assistant(),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
task = PipelineTask(
|
|
||||||
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(f"Client connected")
|
|
||||||
# Kick off the conversation.
|
|
||||||
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()
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
|
||||||
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
|
||||||
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.processors.aggregators.openai_llm_context import OpenAILLMContext
|
|
||||||
from pipecat.runner.types import RunnerArguments
|
|
||||||
from pipecat.runner.utils import create_transport
|
|
||||||
from pipecat.services.cartesia.tts import CartesiaTTSService
|
|
||||||
from pipecat.services.llm_service import FunctionCallParams
|
|
||||||
from pipecat.services.openai_realtime_beta import (
|
|
||||||
InputAudioNoiseReduction,
|
|
||||||
InputAudioTranscription,
|
|
||||||
OpenAIRealtimeBetaLLMService,
|
|
||||||
SemanticTurnDetection,
|
|
||||||
SessionProperties,
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
async def fetch_weather_from_api(params: FunctionCallParams):
|
|
||||||
temperature = 75 if params.arguments["format"] == "fahrenheit" else 24
|
|
||||||
await params.result_callback(
|
|
||||||
{
|
|
||||||
"conditions": "nice",
|
|
||||||
"temperature": temperature,
|
|
||||||
"format": params.arguments["format"],
|
|
||||||
"timestamp": datetime.now().strftime("%Y%m%d_%H%M%S"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def fetch_restaurant_recommendation(params: FunctionCallParams):
|
|
||||||
await params.result_callback({"name": "The Golden Dragon"})
|
|
||||||
|
|
||||||
|
|
||||||
weather_function = FunctionSchema(
|
|
||||||
name="get_current_weather",
|
|
||||||
description="Get the current weather",
|
|
||||||
properties={
|
|
||||||
"location": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The city and state, e.g. San Francisco, CA",
|
|
||||||
},
|
|
||||||
"format": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["celsius", "fahrenheit"],
|
|
||||||
"description": "The temperature unit to use. Infer this from the users location.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required=["location", "format"],
|
|
||||||
)
|
|
||||||
|
|
||||||
restaurant_function = FunctionSchema(
|
|
||||||
name="get_restaurant_recommendation",
|
|
||||||
description="Get a restaurant recommendation",
|
|
||||||
properties={
|
|
||||||
"location": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The city and state, e.g. San Francisco, CA",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required=["location"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create tools schema
|
|
||||||
tools = ToolsSchema(standard_tools=[weather_function, restaurant_function])
|
|
||||||
|
|
||||||
|
|
||||||
# We use lambdas to defer transport parameter creation until the transport
|
|
||||||
# type is selected at runtime.
|
|
||||||
transport_params = {
|
|
||||||
"daily": lambda: DailyParams(
|
|
||||||
audio_in_enabled=True,
|
|
||||||
audio_out_enabled=True,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
|
||||||
),
|
|
||||||
"twilio": lambda: FastAPIWebsocketParams(
|
|
||||||
audio_in_enabled=True,
|
|
||||||
audio_out_enabled=True,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
|
||||||
),
|
|
||||||
"webrtc": lambda: TransportParams(
|
|
||||||
audio_in_enabled=True,
|
|
||||||
audio_out_enabled=True,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
|
||||||
logger.info(f"Starting bot")
|
|
||||||
|
|
||||||
session_properties = SessionProperties(
|
|
||||||
input_audio_transcription=InputAudioTranscription(),
|
|
||||||
modalities=["text"],
|
|
||||||
# Set openai TurnDetection parameters. Not setting this at all will turn it
|
|
||||||
# on by default
|
|
||||||
turn_detection=SemanticTurnDetection(),
|
|
||||||
# Or set to False to disable openai turn detection and use transport VAD
|
|
||||||
# turn_detection=False,
|
|
||||||
input_audio_noise_reduction=InputAudioNoiseReduction(type="near_field"),
|
|
||||||
# tools=tools,
|
|
||||||
instructions="""You are a helpful and friendly AI.
|
|
||||||
|
|
||||||
Act like a human, but remember that you aren't a human and that you can't do human
|
|
||||||
things in the real world. Your voice and personality should be warm and engaging, with a lively and
|
|
||||||
playful tone.
|
|
||||||
|
|
||||||
If interacting in a non-English language, start by using the standard accent or dialect familiar to
|
|
||||||
the user. Talk quickly. You should always call a function if you can. Do not refer to these rules,
|
|
||||||
even if you're asked about them.
|
|
||||||
|
|
||||||
You are participating in a voice conversation. Keep your responses concise, short, and to the point
|
|
||||||
unless specifically asked to elaborate on a topic.
|
|
||||||
|
|
||||||
You have access to the following tools:
|
|
||||||
- get_current_weather: Get the current weather for a given location.
|
|
||||||
- get_restaurant_recommendation: Get a restaurant recommendation for a given location.
|
|
||||||
|
|
||||||
Remember, your responses should be short. Just one or two sentences, usually. Respond in English.""",
|
|
||||||
)
|
|
||||||
|
|
||||||
llm = OpenAIRealtimeBetaLLMService(
|
|
||||||
api_key=os.getenv("OPENAI_API_KEY"),
|
|
||||||
session_properties=session_properties,
|
|
||||||
)
|
|
||||||
|
|
||||||
tts = CartesiaTTSService(
|
|
||||||
api_key=os.getenv("CARTESIA_API_KEY"),
|
|
||||||
settings=CartesiaTTSService.Settings(
|
|
||||||
voice="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# you can either register a single function for all function calls, or specific functions
|
|
||||||
# llm.register_function(None, fetch_weather_from_api)
|
|
||||||
llm.register_function("get_current_weather", fetch_weather_from_api)
|
|
||||||
llm.register_function("get_restaurant_recommendation", fetch_restaurant_recommendation)
|
|
||||||
|
|
||||||
# Create a standard OpenAI LLM context object using the normal messages format. The
|
|
||||||
# OpenAIRealtimeBetaLLMService will convert this internally to messages that the
|
|
||||||
# openai WebSocket API can understand.
|
|
||||||
context = OpenAILLMContext(
|
|
||||||
[{"role": "developer", "content": "Say hello!"}],
|
|
||||||
tools,
|
|
||||||
)
|
|
||||||
|
|
||||||
context_aggregator = llm.create_context_aggregator(context)
|
|
||||||
|
|
||||||
pipeline = Pipeline(
|
|
||||||
[
|
|
||||||
transport.input(), # Transport user input
|
|
||||||
context_aggregator.user(),
|
|
||||||
llm, # LLM
|
|
||||||
tts, # TTS
|
|
||||||
transport.output(), # Transport bot output
|
|
||||||
context_aggregator.assistant(),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
task = PipelineTask(
|
|
||||||
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(f"Client connected")
|
|
||||||
# Kick off the conversation.
|
|
||||||
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()
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
|
||||||
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
|
||||||
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
|
||||||
from pipecat.frames.frames import LLMRunFrame, TranscriptionMessage
|
|
||||||
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.processors.transcript_processor import TranscriptProcessor
|
|
||||||
from pipecat.runner.types import RunnerArguments
|
|
||||||
from pipecat.runner.utils import create_transport
|
|
||||||
from pipecat.services.llm_service import FunctionCallParams
|
|
||||||
from pipecat.services.openai_realtime_beta import (
|
|
||||||
InputAudioNoiseReduction,
|
|
||||||
InputAudioTranscription,
|
|
||||||
OpenAIRealtimeBetaLLMService,
|
|
||||||
SemanticTurnDetection,
|
|
||||||
SessionProperties,
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
async def fetch_weather_from_api(params: FunctionCallParams):
|
|
||||||
temperature = 75 if params.arguments["format"] == "fahrenheit" else 24
|
|
||||||
await params.result_callback(
|
|
||||||
{
|
|
||||||
"conditions": "nice",
|
|
||||||
"temperature": temperature,
|
|
||||||
"format": params.arguments["format"],
|
|
||||||
"timestamp": datetime.now().strftime("%Y%m%d_%H%M%S"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def fetch_restaurant_recommendation(params: FunctionCallParams):
|
|
||||||
await params.result_callback({"name": "The Golden Dragon"})
|
|
||||||
|
|
||||||
|
|
||||||
weather_function = FunctionSchema(
|
|
||||||
name="get_current_weather",
|
|
||||||
description="Get the current weather",
|
|
||||||
properties={
|
|
||||||
"location": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The city and state, e.g. San Francisco, CA",
|
|
||||||
},
|
|
||||||
"format": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["celsius", "fahrenheit"],
|
|
||||||
"description": "The temperature unit to use. Infer this from the users location.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required=["location", "format"],
|
|
||||||
)
|
|
||||||
|
|
||||||
restaurant_function = FunctionSchema(
|
|
||||||
name="get_restaurant_recommendation",
|
|
||||||
description="Get a restaurant recommendation",
|
|
||||||
properties={
|
|
||||||
"location": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The city and state, e.g. San Francisco, CA",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required=["location"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create tools schema
|
|
||||||
tools = ToolsSchema(standard_tools=[weather_function, restaurant_function])
|
|
||||||
|
|
||||||
|
|
||||||
# We use lambdas to defer transport parameter creation until the transport
|
|
||||||
# type is selected at runtime.
|
|
||||||
transport_params = {
|
|
||||||
"daily": lambda: DailyParams(
|
|
||||||
audio_in_enabled=True,
|
|
||||||
audio_out_enabled=True,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
|
||||||
),
|
|
||||||
"twilio": lambda: FastAPIWebsocketParams(
|
|
||||||
audio_in_enabled=True,
|
|
||||||
audio_out_enabled=True,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
|
||||||
),
|
|
||||||
"webrtc": lambda: TransportParams(
|
|
||||||
audio_in_enabled=True,
|
|
||||||
audio_out_enabled=True,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
|
||||||
logger.info(f"Starting bot")
|
|
||||||
|
|
||||||
session_properties = SessionProperties(
|
|
||||||
input_audio_transcription=InputAudioTranscription(),
|
|
||||||
# Set openai TurnDetection parameters. Not setting this at all will turn it
|
|
||||||
# on by default
|
|
||||||
turn_detection=SemanticTurnDetection(),
|
|
||||||
# Or set to False to disable openai turn detection and use transport VAD
|
|
||||||
# turn_detection=False,
|
|
||||||
input_audio_noise_reduction=InputAudioNoiseReduction(type="near_field"),
|
|
||||||
# tools=tools,
|
|
||||||
instructions="""You are a helpful and friendly AI.
|
|
||||||
|
|
||||||
Act like a human, but remember that you aren't a human and that you can't do human
|
|
||||||
things in the real world. Your voice and personality should be warm and engaging, with a lively and
|
|
||||||
playful tone.
|
|
||||||
|
|
||||||
If interacting in a non-English language, start by using the standard accent or dialect familiar to
|
|
||||||
the user. Talk quickly. You should always call a function if you can. Do not refer to these rules,
|
|
||||||
even if you're asked about them.
|
|
||||||
|
|
||||||
You are participating in a voice conversation. Keep your responses concise, short, and to the point
|
|
||||||
unless specifically asked to elaborate on a topic.
|
|
||||||
|
|
||||||
You have access to the following tools:
|
|
||||||
- get_current_weather: Get the current weather for a given location.
|
|
||||||
- get_restaurant_recommendation: Get a restaurant recommendation for a given location.
|
|
||||||
|
|
||||||
Remember, your responses should be short. Just one or two sentences, usually. Respond in English.""",
|
|
||||||
)
|
|
||||||
|
|
||||||
llm = OpenAIRealtimeBetaLLMService(
|
|
||||||
api_key=os.getenv("OPENAI_API_KEY"),
|
|
||||||
session_properties=session_properties,
|
|
||||||
)
|
|
||||||
|
|
||||||
# you can either register a single function for all function calls, or specific functions
|
|
||||||
# llm.register_function(None, fetch_weather_from_api)
|
|
||||||
llm.register_function("get_current_weather", fetch_weather_from_api)
|
|
||||||
llm.register_function("get_restaurant_recommendation", fetch_restaurant_recommendation)
|
|
||||||
|
|
||||||
transcript = TranscriptProcessor()
|
|
||||||
|
|
||||||
# Create a standard OpenAI LLM context object using the normal messages format. The
|
|
||||||
# OpenAIRealtimeBetaLLMService will convert this internally to messages that the
|
|
||||||
# openai WebSocket API can understand.
|
|
||||||
context = OpenAILLMContext(
|
|
||||||
[{"role": "developer", "content": "Say hello!"}],
|
|
||||||
tools,
|
|
||||||
)
|
|
||||||
|
|
||||||
context_aggregator = llm.create_context_aggregator(context)
|
|
||||||
|
|
||||||
pipeline = Pipeline(
|
|
||||||
[
|
|
||||||
transport.input(), # Transport user input
|
|
||||||
context_aggregator.user(),
|
|
||||||
llm, # LLM
|
|
||||||
transcript.user(), # Placed after the LLM, as LLM pushes TranscriptionFrames downstream
|
|
||||||
transport.output(), # Transport bot output
|
|
||||||
transcript.assistant(), # After the transcript output, to time with the audio output
|
|
||||||
context_aggregator.assistant(),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
task = PipelineTask(
|
|
||||||
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(f"Client connected")
|
|
||||||
# Kick off the conversation.
|
|
||||||
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()
|
|
||||||
|
|
||||||
# Register event handler for transcript updates
|
|
||||||
@transcript.event_handler("on_transcript_update")
|
|
||||||
async def on_transcript_update(processor, frame):
|
|
||||||
for msg in frame.messages:
|
|
||||||
if isinstance(msg, TranscriptionMessage):
|
|
||||||
timestamp = f"[{msg.timestamp}] " if msg.timestamp else ""
|
|
||||||
line = f"{timestamp}{msg.role}: {msg.content}"
|
|
||||||
logger.info(f"Transcript: {line}")
|
|
||||||
|
|
||||||
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()
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Deprecated AI services module.
|
|
||||||
|
|
||||||
This module is deprecated. Import services directly from their respective modules:
|
|
||||||
- pipecat.services.ai_service
|
|
||||||
- pipecat.services.image_service
|
|
||||||
- pipecat.services.llm_service
|
|
||||||
- pipecat.services.stt_service
|
|
||||||
- pipecat.services.tts_service
|
|
||||||
- pipecat.services.vision_service
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pipecat.services import DeprecatedModuleProxy
|
|
||||||
|
|
||||||
from .ai_service import *
|
|
||||||
from .image_service import *
|
|
||||||
from .llm_service import *
|
|
||||||
from .stt_service import *
|
|
||||||
from .tts_service import *
|
|
||||||
from .vision_service import *
|
|
||||||
|
|
||||||
sys.modules[__name__] = DeprecatedModuleProxy(
|
|
||||||
globals(),
|
|
||||||
"ai_services",
|
|
||||||
"[ai_service,image_service,llm_service,stt_service,tts_service,vision_service]",
|
|
||||||
)
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pipecat.services.aws.nova_sonic.llm import AWSNovaSonicLLMService, Params
|
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
warnings.warn(
|
|
||||||
"Types in pipecat.services.aws_nova_sonic are deprecated. "
|
|
||||||
"Please use the equivalent types from "
|
|
||||||
"pipecat.services.aws.nova_sonic.llm instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"AWSNovaSonicLLMService",
|
|
||||||
"Params",
|
|
||||||
]
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""AWS Nova Sonic LLM service implementation for Pipecat AI framework.
|
|
||||||
|
|
||||||
This module provides a speech-to-speech LLM service using AWS Nova Sonic, which supports
|
|
||||||
bidirectional audio streaming, text generation, and function calling capabilities.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pipecat.services.aws.nova_sonic.llm import *
|
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
warnings.warn(
|
|
||||||
"Types in pipecat.services.aws_nova_sonic.aws are deprecated. "
|
|
||||||
"Please use the equivalent types from "
|
|
||||||
"pipecat.services.aws.nova_sonic.llm instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Context management for AWS Nova Sonic LLM service.
|
|
||||||
|
|
||||||
This module provides specialized context aggregators and message handling for AWS Nova Sonic,
|
|
||||||
including conversation history management and role-specific message processing.
|
|
||||||
|
|
||||||
.. deprecated:: 0.0.91
|
|
||||||
AWS Nova Sonic no longer uses types from this module under the hood.
|
|
||||||
It now uses `LLMContext` and `LLMContextAggregatorPair`.
|
|
||||||
Using the new patterns should allow you to not need types from this module.
|
|
||||||
|
|
||||||
See deprecation warning in pipecat.services.aws.nova_sonic.context for more
|
|
||||||
details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pipecat.services.aws.nova_sonic.context import *
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Custom frames for AWS Nova Sonic LLM service."""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pipecat.services.aws.nova_sonic.frames import *
|
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
warnings.warn(
|
|
||||||
"Types in pipecat.services.aws_nova_sonic.frames are deprecated. "
|
|
||||||
"Please use the equivalent types from "
|
|
||||||
"pipecat.services.aws.nova_sonic.frames instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Deprecated: use ``pipecat.services.deepgram.sagemaker.stt`` instead."""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"Module `pipecat.services.deepgram.stt_sagemaker` is deprecated, "
|
|
||||||
"use `pipecat.services.deepgram.sagemaker.stt` instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
from pipecat.services.deepgram.sagemaker.stt import * # noqa: E402, F401, F403
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Deprecated: use ``pipecat.services.deepgram.sagemaker.tts`` instead."""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"Module `pipecat.services.deepgram.tts_sagemaker` is deprecated, "
|
|
||||||
"use `pipecat.services.deepgram.sagemaker.tts` instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
from pipecat.services.deepgram.sagemaker.tts import * # noqa: E402, F401, F403
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from .file_api import GeminiFileAPI
|
|
||||||
from .gemini import GeminiMultimodalLiveLLMService
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"GeminiFileAPI",
|
|
||||||
"GeminiMultimodalLiveLLMService",
|
|
||||||
]
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Event models and utilities for Google Gemini Multimodal Live API.
|
|
||||||
|
|
||||||
.. deprecated:: 0.0.90
|
|
||||||
Importing StartSensitivity and EndSensitivity from this module is deprecated.
|
|
||||||
Import them directly from google.genai.types instead.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
try:
|
|
||||||
from google.genai.types import (
|
|
||||||
EndSensitivity as _EndSensitivity,
|
|
||||||
)
|
|
||||||
from google.genai.types import (
|
|
||||||
StartSensitivity as _StartSensitivity,
|
|
||||||
)
|
|
||||||
except ModuleNotFoundError as e:
|
|
||||||
logger.error(f"Exception: {e}")
|
|
||||||
logger.error("In order to use Google AI, you need to `pip install pipecat-ai[google]`.")
|
|
||||||
raise Exception(f"Missing module: {e}")
|
|
||||||
|
|
||||||
# These aliases are just here for backward compatibility, since we used to
|
|
||||||
# define public-facing StartSensitivity and EndSensitivity enums in this
|
|
||||||
# module.
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
warnings.warn(
|
|
||||||
"Importing StartSensitivity and EndSensitivity from "
|
|
||||||
"pipecat.services.gemini_multimodal_live.events is deprecated. "
|
|
||||||
"Please import them directly from google.genai.types instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
StartSensitivity = _StartSensitivity
|
|
||||||
EndSensitivity = _EndSensitivity
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Gemini File API client for uploading and managing files.
|
|
||||||
|
|
||||||
This module provides a client for Google's Gemini File API, enabling file
|
|
||||||
uploads, metadata retrieval, listing, and deletion. Files uploaded through
|
|
||||||
this API can be referenced in Gemini generative model calls.
|
|
||||||
|
|
||||||
.. deprecated:: 0.0.90
|
|
||||||
Importing GeminiFileAPI from this module is deprecated.
|
|
||||||
Import it from pipecat.services.google.gemini_live.file_api instead.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
try:
|
|
||||||
from pipecat.services.google.gemini_live.file_api import GeminiFileAPI as _GeminiFileAPI
|
|
||||||
except ModuleNotFoundError as e:
|
|
||||||
logger.error(f"Exception: {e}")
|
|
||||||
logger.error("In order to use Google AI, you need to `pip install pipecat-ai[google]`.")
|
|
||||||
raise Exception(f"Missing module: {e}")
|
|
||||||
|
|
||||||
# These aliases are just here for backward compatibility, since we used to
|
|
||||||
# define public-facing StartSensitivity and EndSensitivity enums in this
|
|
||||||
# module.
|
|
||||||
warnings.warn(
|
|
||||||
"Importing GeminiFileAPI from "
|
|
||||||
"pipecat.services.gemini_multimodal_live.file_api is deprecated. "
|
|
||||||
"Please import it from pipecat.services.google.gemini_live.file_api instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
GeminiFileAPI = _GeminiFileAPI
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Google Gemini Live API service implementation.
|
|
||||||
|
|
||||||
This module provides real-time conversational AI capabilities using Google's
|
|
||||||
Gemini Live API, supporting both text and audio modalities with
|
|
||||||
voice transcription, streaming responses, and tool usage.
|
|
||||||
|
|
||||||
.. deprecated:: 0.0.90
|
|
||||||
This module is deprecated. Please use the equivalent types from
|
|
||||||
pipecat.services.google.gemini_live.llm instead. Note that the new type names
|
|
||||||
do not include 'Multimodal'.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pipecat.services.google.gemini_live.llm import (
|
|
||||||
ContextWindowCompressionParams as _ContextWindowCompressionParams,
|
|
||||||
)
|
|
||||||
from pipecat.services.google.gemini_live.llm import (
|
|
||||||
GeminiLiveAssistantContextAggregator,
|
|
||||||
GeminiLiveContext,
|
|
||||||
GeminiLiveContextAggregatorPair,
|
|
||||||
GeminiLiveLLMService,
|
|
||||||
GeminiLiveUserContextAggregator,
|
|
||||||
GeminiModalities,
|
|
||||||
)
|
|
||||||
from pipecat.services.google.gemini_live.llm import GeminiMediaResolution as _GeminiMediaResolution
|
|
||||||
from pipecat.services.google.gemini_live.llm import GeminiVADParams as _GeminiVADParams
|
|
||||||
from pipecat.services.google.gemini_live.llm import InputParams as _InputParams
|
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
warnings.warn(
|
|
||||||
"Types in pipecat.services.gemini_multimodal_live.gemini are deprecated. "
|
|
||||||
"Please use the equivalent types from "
|
|
||||||
"pipecat.services.google.gemini_live.llm instead. Note that the new type "
|
|
||||||
"names do not include 'Multimodal' "
|
|
||||||
"(e.g. `GeminiMultimodalLiveLLMService` is now `GeminiLiveLLMService`).",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
GeminiMultimodalLiveContext = GeminiLiveContext
|
|
||||||
GeminiMultimodalLiveUserContextAggregator = GeminiLiveUserContextAggregator
|
|
||||||
GeminiMultimodalLiveAssistantContextAggregator = GeminiLiveAssistantContextAggregator
|
|
||||||
GeminiMultimodalLiveContextAggregatorPair = GeminiLiveContextAggregatorPair
|
|
||||||
GeminiMultimodalModalities = GeminiModalities
|
|
||||||
GeminiMediaResolution = _GeminiMediaResolution
|
|
||||||
GeminiVADParams = _GeminiVADParams
|
|
||||||
ContextWindowCompressionParams = _ContextWindowCompressionParams
|
|
||||||
InputParams = _InputParams
|
|
||||||
GeminiMultimodalLiveLLMService = GeminiLiveLLMService
|
|
||||||
@@ -12,12 +12,11 @@ from .frames import *
|
|||||||
from .gemini_live import *
|
from .gemini_live import *
|
||||||
from .image import *
|
from .image import *
|
||||||
from .llm import *
|
from .llm import *
|
||||||
from .openai import *
|
|
||||||
from .rtvi import *
|
from .rtvi import *
|
||||||
from .stt import *
|
from .stt import *
|
||||||
from .tts import *
|
from .tts import *
|
||||||
from .vertex import *
|
from .vertex import *
|
||||||
|
|
||||||
sys.modules[__name__] = DeprecatedModuleProxy(
|
sys.modules[__name__] = DeprecatedModuleProxy(
|
||||||
globals(), "google", "google.[frames,image,llm,openai,vertex,rtvi,stt,tts]"
|
globals(), "google", "google.[frames,image,llm,vertex,rtvi,stt,tts]"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Deprecated: use ``pipecat.services.google.gemini_live.vertex.llm`` instead."""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"Module `pipecat.services.google.gemini_live.llm_vertex` is deprecated, "
|
|
||||||
"use `pipecat.services.google.gemini_live.vertex.llm` instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
from pipecat.services.google.gemini_live.vertex.llm import * # noqa: E402, F401, F403
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Google services module for Pipecat."""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pipecat.services import DeprecatedModuleProxy
|
|
||||||
|
|
||||||
from .frames import *
|
|
||||||
from .image import *
|
|
||||||
from .llm import *
|
|
||||||
from .openai import *
|
|
||||||
from .rtvi import *
|
|
||||||
from .stt import *
|
|
||||||
from .tts import *
|
|
||||||
from .vertex import *
|
|
||||||
|
|
||||||
sys.modules[__name__] = DeprecatedModuleProxy(
|
|
||||||
globals(), "google", "google.[frames,image,llm,openai,vertex,rtvi,stt,tts]"
|
|
||||||
)
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Deprecated: use ``pipecat.services.google.openai.llm`` instead."""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"Module `pipecat.services.google.llm_openai` is deprecated, "
|
|
||||||
"use `pipecat.services.google.openai.llm` instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
from pipecat.services.google.openai.llm import * # noqa: E402, F401, F403
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Deprecated: use ``pipecat.services.google.vertex.llm`` instead."""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"Module `pipecat.services.google.llm_vertex` is deprecated, "
|
|
||||||
"use `pipecat.services.google.vertex.llm` instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
from pipecat.services.google.vertex.llm import * # noqa: E402, F401, F403
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Google LLM service using OpenAI-compatible API format.
|
|
||||||
|
|
||||||
This module provides integration with Google's AI LLM models using the OpenAI
|
|
||||||
API format through Google's Gemini API OpenAI compatibility layer.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from openai import AsyncStream
|
|
||||||
from openai.types.chat import ChatCompletionChunk
|
|
||||||
|
|
||||||
from pipecat.services.llm_service import FunctionCallFromLLM
|
|
||||||
|
|
||||||
# Suppress gRPC fork warnings
|
|
||||||
os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
|
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from pipecat.frames.frames import LLMTextFrame
|
|
||||||
from pipecat.metrics.metrics import LLMTokenUsage
|
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
|
||||||
from pipecat.services.openai.base_llm import BaseOpenAILLMService
|
|
||||||
from pipecat.services.openai.llm import OpenAILLMService
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class GoogleOpenAILLMSettings(BaseOpenAILLMService.Settings):
|
|
||||||
"""Settings for GoogleLLMOpenAIBetaService."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class GoogleLLMOpenAIBetaService(OpenAILLMService):
|
|
||||||
"""Google LLM service using OpenAI-compatible API format.
|
|
||||||
|
|
||||||
This service provides access to Google's AI LLM models (like Gemini) through
|
|
||||||
the OpenAI API format. It handles streaming responses, function calls, and
|
|
||||||
tool usage while maintaining compatibility with OpenAI's interface.
|
|
||||||
|
|
||||||
Note: This service includes a workaround for a Google API bug where function
|
|
||||||
call indices may be incorrectly set to None, resulting in empty function names.
|
|
||||||
|
|
||||||
.. deprecated:: 0.0.82
|
|
||||||
GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version.
|
|
||||||
Use GoogleLLMService instead for better integration with Google's native API.
|
|
||||||
|
|
||||||
Reference:
|
|
||||||
https://ai.google.dev/gemini-api/docs/openai
|
|
||||||
"""
|
|
||||||
|
|
||||||
Settings = GoogleOpenAILLMSettings
|
|
||||||
_settings: Settings
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
api_key: str,
|
|
||||||
base_url: str = "https://generativelanguage.googleapis.com/v1beta/openai/",
|
|
||||||
model: Optional[str] = None,
|
|
||||||
settings: Optional[Settings] = None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""Initialize the Google LLM service.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
api_key: Google API key for authentication.
|
|
||||||
base_url: Base URL for Google's OpenAI-compatible API.
|
|
||||||
model: Google model name to use (e.g., "gemini-2.0-flash").
|
|
||||||
|
|
||||||
.. deprecated:: 0.0.105
|
|
||||||
Use ``settings=GoogleLLMOpenAIBetaService.Settings(model=...)`` instead.
|
|
||||||
|
|
||||||
settings: Runtime-updatable settings. When provided alongside deprecated
|
|
||||||
parameters, ``settings`` values take precedence.
|
|
||||||
**kwargs: Additional arguments passed to the parent OpenAILLMService.
|
|
||||||
"""
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
warnings.warn(
|
|
||||||
"GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version. "
|
|
||||||
"Use GoogleLLMService instead for better integration with Google's native API.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 1. Initialize default_settings with hardcoded defaults
|
|
||||||
default_settings = self.Settings(model="gemini-2.0-flash")
|
|
||||||
|
|
||||||
# 2. Apply direct init arg overrides (deprecated)
|
|
||||||
if model is not None:
|
|
||||||
self._warn_init_param_moved_to_settings("model", "model")
|
|
||||||
default_settings.model = model
|
|
||||||
|
|
||||||
# 3. (No step 3, as there's no params object to apply)
|
|
||||||
|
|
||||||
# 4. Apply settings delta (canonical API, always wins)
|
|
||||||
if settings is not None:
|
|
||||||
default_settings.apply_update(settings)
|
|
||||||
|
|
||||||
super().__init__(api_key=api_key, base_url=base_url, settings=default_settings, **kwargs)
|
|
||||||
|
|
||||||
async def _process_context(self, context: OpenAILLMContext):
|
|
||||||
functions_list = []
|
|
||||||
arguments_list = []
|
|
||||||
tool_id_list = []
|
|
||||||
func_idx = 0
|
|
||||||
function_name = ""
|
|
||||||
arguments = ""
|
|
||||||
tool_call_id = ""
|
|
||||||
|
|
||||||
await self.start_ttfb_metrics()
|
|
||||||
|
|
||||||
chunk_stream: AsyncStream[
|
|
||||||
ChatCompletionChunk
|
|
||||||
] = await self._stream_chat_completions_specific_context(context)
|
|
||||||
|
|
||||||
# Use context manager to ensure stream is closed on cancellation/exception.
|
|
||||||
# Without this, CancelledError during iteration leaves the underlying socket open.
|
|
||||||
async with chunk_stream:
|
|
||||||
async for chunk in chunk_stream:
|
|
||||||
if chunk.usage:
|
|
||||||
tokens = LLMTokenUsage(
|
|
||||||
prompt_tokens=chunk.usage.prompt_tokens or 0,
|
|
||||||
completion_tokens=chunk.usage.completion_tokens or 0,
|
|
||||||
total_tokens=chunk.usage.total_tokens or 0,
|
|
||||||
)
|
|
||||||
await self.start_llm_usage_metrics(tokens)
|
|
||||||
|
|
||||||
if chunk.choices is None or len(chunk.choices) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
await self.stop_ttfb_metrics()
|
|
||||||
|
|
||||||
if not chunk.choices[0].delta:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if chunk.choices[0].delta.tool_calls:
|
|
||||||
# We're streaming the LLM response to enable the fastest response times.
|
|
||||||
# For text, we just yield each chunk as we receive it and count on consumers
|
|
||||||
# to do whatever coalescing they need (eg. to pass full sentences to TTS)
|
|
||||||
#
|
|
||||||
# If the LLM is a function call, we'll do some coalescing here.
|
|
||||||
# If the response contains a function name, we'll yield a frame to tell consumers
|
|
||||||
# that they can start preparing to call the function with that name.
|
|
||||||
# We accumulate all the arguments for the rest of the streamed response, then when
|
|
||||||
# the response is done, we package up all the arguments and the function name and
|
|
||||||
# yield a frame containing the function name and the arguments.
|
|
||||||
logger.debug(f"Tool call: {chunk.choices[0].delta.tool_calls}")
|
|
||||||
tool_call = chunk.choices[0].delta.tool_calls[0]
|
|
||||||
if tool_call.index != func_idx:
|
|
||||||
functions_list.append(function_name)
|
|
||||||
arguments_list.append(arguments)
|
|
||||||
tool_id_list.append(tool_call_id)
|
|
||||||
function_name = ""
|
|
||||||
arguments = ""
|
|
||||||
tool_call_id = ""
|
|
||||||
func_idx += 1
|
|
||||||
if tool_call.function and tool_call.function.name:
|
|
||||||
function_name += tool_call.function.name
|
|
||||||
tool_call_id = tool_call.id
|
|
||||||
if tool_call.function and tool_call.function.arguments:
|
|
||||||
# Keep iterating through the response to collect all the argument fragments
|
|
||||||
arguments += tool_call.function.arguments
|
|
||||||
elif chunk.choices[0].delta.content:
|
|
||||||
await self.push_frame(LLMTextFrame(chunk.choices[0].delta.content))
|
|
||||||
|
|
||||||
# if we got a function name and arguments, check to see if it's a function with
|
|
||||||
# a registered handler. If so, run the registered callback, save the result to
|
|
||||||
# the context, and re-prompt to get a chat answer. If we don't have a registered
|
|
||||||
# handler, raise an exception.
|
|
||||||
if function_name and arguments:
|
|
||||||
# added to the list as last function name and arguments not added to the list
|
|
||||||
functions_list.append(function_name)
|
|
||||||
arguments_list.append(arguments)
|
|
||||||
tool_id_list.append(tool_call_id)
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
f"Function list: {functions_list}, Arguments list: {arguments_list}, Tool ID list: {tool_id_list}"
|
|
||||||
)
|
|
||||||
|
|
||||||
function_calls = []
|
|
||||||
for function_name, arguments, tool_id in zip(
|
|
||||||
functions_list, arguments_list, tool_id_list
|
|
||||||
):
|
|
||||||
if function_name == "":
|
|
||||||
# TODO: Remove the _process_context method once Google resolves the bug
|
|
||||||
# where the index is incorrectly set to None instead of returning the actual index,
|
|
||||||
# which currently results in an empty function name('').
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
arguments = json.loads(arguments)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
logger.warning(f"{self}: Failed to parse function call arguments: {arguments}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
function_calls.append(
|
|
||||||
FunctionCallFromLLM(
|
|
||||||
context=context,
|
|
||||||
tool_call_id=tool_id,
|
|
||||||
function_name=function_name,
|
|
||||||
arguments=arguments,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.run_function_calls(function_calls)
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
"""Grok LLM service implementation.
|
"""Grok LLM service implementation.
|
||||||
|
|
||||||
.. deprecated::
|
.. deprecated:: 0.0.108
|
||||||
This module is deprecated. Please use GrokLLMService from
|
This module is deprecated. Please use GrokLLMService from
|
||||||
pipecat.services.xai.llm instead.
|
pipecat.services.xai.llm instead.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
"""Grok Realtime event models.
|
"""Grok Realtime event models.
|
||||||
|
|
||||||
.. deprecated::
|
.. deprecated:: 0.0.108
|
||||||
This module is deprecated. Please use pipecat.services.xai.realtime.events instead.
|
This module is deprecated. Please use pipecat.services.xai.realtime.events instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
"""Grok Realtime LLM service.
|
"""Grok Realtime LLM service.
|
||||||
|
|
||||||
.. deprecated::
|
.. deprecated:: 0.0.108
|
||||||
This module is deprecated. Please use GrokRealtimeLLMService from
|
This module is deprecated. Please use GrokRealtimeLLMService from
|
||||||
pipecat.services.xai.realtime.llm instead.
|
pipecat.services.xai.realtime.llm instead.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pipecat.services import DeprecatedModuleProxy
|
|
||||||
|
|
||||||
from .llm import *
|
|
||||||
|
|
||||||
sys.modules[__name__] = DeprecatedModuleProxy(globals(), "nim", "nim.llm")
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""NVIDIA NIM API service implementation.
|
|
||||||
|
|
||||||
This module provides a service for interacting with NVIDIA's NIM (NVIDIA Inference
|
|
||||||
Microservice) API while maintaining compatibility with the OpenAI-style interface.
|
|
||||||
|
|
||||||
.. deprecated:: 0.0.96
|
|
||||||
This module is deprecated. Please NvidiaLLMService from
|
|
||||||
pipecat.services.nvidia.llm instead.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pipecat.services.nvidia.llm import NvidiaLLMService
|
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
warnings.warn(
|
|
||||||
"NimLLMService from pipecat.services.nim.llm is deprecated. "
|
|
||||||
"Please use NvidiaLLMService from pipecat.services.nvidia.llm instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
NimLLMService = NvidiaLLMService
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pipecat.services.azure.realtime.llm import AzureRealtimeLLMService
|
|
||||||
from pipecat.services.openai.realtime.events import (
|
|
||||||
InputAudioNoiseReduction,
|
|
||||||
InputAudioTranscription,
|
|
||||||
SemanticTurnDetection,
|
|
||||||
SessionProperties,
|
|
||||||
TurnDetection,
|
|
||||||
)
|
|
||||||
from pipecat.services.openai.realtime.llm import OpenAIRealtimeLLMService
|
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
warnings.warn(
|
|
||||||
"Types in pipecat.services.openai_realtime are deprecated. "
|
|
||||||
"Please use the equivalent types from "
|
|
||||||
"pipecat.services.openai.realtime instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"AzureRealtimeLLMService",
|
|
||||||
"InputAudioNoiseReduction",
|
|
||||||
"InputAudioTranscription",
|
|
||||||
"SemanticTurnDetection",
|
|
||||||
"SessionProperties",
|
|
||||||
"TurnDetection",
|
|
||||||
"OpenAIRealtimeLLMService",
|
|
||||||
]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Azure OpenAI Realtime LLM service implementation."""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pipecat.services.azure.realtime.llm import *
|
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
warnings.warn(
|
|
||||||
"Types in pipecat.services.openai_realtime.azure are deprecated. "
|
|
||||||
"Please use the equivalent types from "
|
|
||||||
"pipecat.services.azure.realtime.llm instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""OpenAI Realtime LLM context and aggregator implementations.
|
|
||||||
|
|
||||||
.. deprecated:: 0.0.91
|
|
||||||
OpenAI Realtime no longer uses types from this module under the hood.
|
|
||||||
It now uses `LLMContext` and `LLMContextAggregatorPair`.
|
|
||||||
Using the new patterns should allow you to not need types from this module.
|
|
||||||
|
|
||||||
See deprecation warning in pipecat.services.openai.realtime.context for
|
|
||||||
more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pipecat.services.openai.realtime.context import *
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Event models and data structures for OpenAI Realtime API communication."""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pipecat.services.openai.realtime.events import *
|
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
warnings.warn(
|
|
||||||
"Types in pipecat.services.openai_realtime.events are deprecated. "
|
|
||||||
"Please use the equivalent types from "
|
|
||||||
"pipecat.services.openai.realtime.events instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Custom frame types for OpenAI Realtime API integration."""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pipecat.services.openai.realtime.frames import *
|
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
warnings.warn(
|
|
||||||
"Types in pipecat.services.openai_realtime.frames are deprecated. "
|
|
||||||
"Please use the equivalent types from "
|
|
||||||
"pipecat.services.openai.realtime.frames instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from .azure import AzureRealtimeBetaLLMService
|
|
||||||
from .events import (
|
|
||||||
InputAudioNoiseReduction,
|
|
||||||
InputAudioTranscription,
|
|
||||||
SemanticTurnDetection,
|
|
||||||
SessionProperties,
|
|
||||||
TurnDetection,
|
|
||||||
)
|
|
||||||
from .openai import OpenAIRealtimeBetaLLMService
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"AzureRealtimeBetaLLMService",
|
|
||||||
"InputAudioNoiseReduction",
|
|
||||||
"InputAudioTranscription",
|
|
||||||
"SemanticTurnDetection",
|
|
||||||
"SessionProperties",
|
|
||||||
"TurnDetection",
|
|
||||||
"OpenAIRealtimeBetaLLMService",
|
|
||||||
]
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Azure OpenAI Realtime Beta LLM service implementation."""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from .openai import OpenAIRealtimeBetaLLMService
|
|
||||||
|
|
||||||
try:
|
|
||||||
from websockets.asyncio.client import connect as websocket_connect
|
|
||||||
except ModuleNotFoundError as e:
|
|
||||||
logger.error(f"Exception: {e}")
|
|
||||||
logger.error(
|
|
||||||
"In order to use OpenAI, you need to `pip install pipecat-ai[openai]`. Also, set `OPENAI_API_KEY` environment variable."
|
|
||||||
)
|
|
||||||
raise Exception(f"Missing module: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AzureRealtimeBetaLLMSettings(OpenAIRealtimeBetaLLMService.Settings):
|
|
||||||
"""Settings for AzureRealtimeBetaLLMService."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AzureRealtimeBetaLLMService(OpenAIRealtimeBetaLLMService):
|
|
||||||
"""Azure OpenAI Realtime Beta LLM service with Azure-specific authentication.
|
|
||||||
|
|
||||||
.. deprecated:: 0.0.84
|
|
||||||
`AzureRealtimeBetaLLMService` is deprecated, use `AzureRealtimeLLMService` instead.
|
|
||||||
This class will be removed in version 1.0.0.
|
|
||||||
|
|
||||||
Extends the OpenAI Realtime service to work with Azure OpenAI endpoints,
|
|
||||||
using Azure's authentication headers and endpoint format. Provides the same
|
|
||||||
real-time audio and text communication capabilities as the base OpenAI service.
|
|
||||||
"""
|
|
||||||
|
|
||||||
Settings = AzureRealtimeBetaLLMSettings
|
|
||||||
_settings: Settings
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
api_key: str,
|
|
||||||
base_url: str,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""Initialize Azure Realtime Beta LLM service.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
api_key: The API key for the Azure OpenAI service.
|
|
||||||
base_url: The full Azure WebSocket endpoint URL including api-version and deployment.
|
|
||||||
Example: "wss://my-project.openai.azure.com/openai/realtime?api-version=2024-10-01-preview&deployment=my-realtime-deployment"
|
|
||||||
**kwargs: Additional arguments passed to parent OpenAIRealtimeBetaLLMService.
|
|
||||||
"""
|
|
||||||
super().__init__(base_url=base_url, api_key=api_key, **kwargs)
|
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
warnings.warn(
|
|
||||||
"AzureRealtimeBetaLLMService is deprecated and will be removed in version 1.0.0. "
|
|
||||||
"Use AzureRealtimeLLMService instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.api_key = api_key
|
|
||||||
self.base_url = base_url
|
|
||||||
|
|
||||||
async def _connect(self):
|
|
||||||
try:
|
|
||||||
if self._websocket:
|
|
||||||
# Here we assume that if we have a websocket, we are connected. We
|
|
||||||
# handle disconnections in the send/recv code paths.
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(f"Connecting to {self.base_url}")
|
|
||||||
self._websocket = await websocket_connect(
|
|
||||||
uri=self.base_url,
|
|
||||||
additional_headers={
|
|
||||||
"api-key": self.api_key,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self._receive_task = self.create_task(self._receive_task_handler())
|
|
||||||
except Exception as e:
|
|
||||||
await self.push_error(error_msg=f"Error connecting: {e}", exception=e)
|
|
||||||
self._websocket = None
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""OpenAI Realtime LLM context and aggregator implementations."""
|
|
||||||
|
|
||||||
import copy
|
|
||||||
import json
|
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from pipecat.frames.frames import (
|
|
||||||
Frame,
|
|
||||||
FunctionCallResultFrame,
|
|
||||||
InterimTranscriptionFrame,
|
|
||||||
LLMMessagesUpdateFrame,
|
|
||||||
LLMSetToolsFrame,
|
|
||||||
LLMTextFrame,
|
|
||||||
TranscriptionFrame,
|
|
||||||
)
|
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
|
||||||
from pipecat.processors.frame_processor import FrameDirection
|
|
||||||
from pipecat.services.openai.llm import (
|
|
||||||
OpenAIAssistantContextAggregator,
|
|
||||||
OpenAIUserContextAggregator,
|
|
||||||
)
|
|
||||||
|
|
||||||
from . import events
|
|
||||||
from .frames import RealtimeFunctionCallResultFrame, RealtimeMessagesUpdateFrame
|
|
||||||
|
|
||||||
|
|
||||||
class OpenAIRealtimeLLMContext(OpenAILLMContext):
|
|
||||||
"""OpenAI Realtime LLM context with session management and message conversion.
|
|
||||||
|
|
||||||
Extends the standard OpenAI LLM context to support real-time session properties,
|
|
||||||
instruction management, and conversion between standard message formats and
|
|
||||||
realtime conversation items.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, messages=None, tools=None, **kwargs):
|
|
||||||
"""Initialize the OpenAIRealtimeLLMContext.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
messages: Initial conversation messages. Defaults to None.
|
|
||||||
tools: Available function tools. Defaults to None.
|
|
||||||
**kwargs: Additional arguments passed to parent OpenAILLMContext.
|
|
||||||
"""
|
|
||||||
super().__init__(messages=messages, tools=tools, **kwargs)
|
|
||||||
self.__setup_local()
|
|
||||||
|
|
||||||
def __setup_local(self):
|
|
||||||
self.llm_needs_settings_update = True
|
|
||||||
self.llm_needs_initial_messages = True
|
|
||||||
self._session_instructions = ""
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def upgrade_to_realtime(obj: OpenAILLMContext) -> "OpenAIRealtimeLLMContext":
|
|
||||||
"""Upgrade a standard OpenAI LLM context to a realtime context.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
obj: The OpenAILLMContext instance to upgrade.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The upgraded OpenAIRealtimeLLMContext instance.
|
|
||||||
"""
|
|
||||||
if isinstance(obj, OpenAILLMContext) and not isinstance(obj, OpenAIRealtimeLLMContext):
|
|
||||||
obj.__class__ = OpenAIRealtimeLLMContext
|
|
||||||
obj.__setup_local()
|
|
||||||
return obj
|
|
||||||
|
|
||||||
# todo
|
|
||||||
# - finish implementing all frames
|
|
||||||
|
|
||||||
def from_standard_message(self, message):
|
|
||||||
"""Convert a standard message format to a realtime conversation item.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: The standard message dictionary to convert.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A ConversationItem instance for the realtime API.
|
|
||||||
"""
|
|
||||||
if message.get("role") == "user":
|
|
||||||
content = message.get("content")
|
|
||||||
if isinstance(message.get("content"), list):
|
|
||||||
content = ""
|
|
||||||
for c in message.get("content"):
|
|
||||||
if c.get("type") == "text":
|
|
||||||
content += " " + c.get("text")
|
|
||||||
else:
|
|
||||||
logger.error(
|
|
||||||
f"Unhandled content type in context message: {c.get('type')} - {message}"
|
|
||||||
)
|
|
||||||
return events.ConversationItem(
|
|
||||||
role="user",
|
|
||||||
type="message",
|
|
||||||
content=[events.ItemContent(type="input_text", text=content)],
|
|
||||||
)
|
|
||||||
if message.get("role") == "assistant" and message.get("tool_calls"):
|
|
||||||
tc = message.get("tool_calls")[0]
|
|
||||||
return events.ConversationItem(
|
|
||||||
type="function_call",
|
|
||||||
call_id=tc["id"],
|
|
||||||
name=tc["function"]["name"],
|
|
||||||
arguments=tc["function"]["arguments"],
|
|
||||||
)
|
|
||||||
logger.error(f"Unhandled message type in from_standard_message: {message}")
|
|
||||||
|
|
||||||
def get_messages_for_initializing_history(self):
|
|
||||||
"""Get conversation items for initializing the realtime session history.
|
|
||||||
|
|
||||||
Converts the context's messages to a format suitable for the realtime API,
|
|
||||||
handling system instructions and conversation history packaging.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of conversation items for session initialization.
|
|
||||||
"""
|
|
||||||
# We can't load a long conversation history into the openai realtime api yet. (The API/model
|
|
||||||
# forgets that it can do audio, if you do a series of `conversation.item.create` calls.) So
|
|
||||||
# our general strategy until this is fixed is just to put everything into a first "user"
|
|
||||||
# message as a single input.
|
|
||||||
if not self.messages:
|
|
||||||
return []
|
|
||||||
|
|
||||||
messages = copy.deepcopy(self.messages)
|
|
||||||
|
|
||||||
# If we have a "system" message as our first message, let's pull that out into session
|
|
||||||
# "instructions"
|
|
||||||
if messages[0].get("role") == "system":
|
|
||||||
self.llm_needs_settings_update = True
|
|
||||||
system = messages.pop(0)
|
|
||||||
content = system.get("content")
|
|
||||||
if isinstance(content, str):
|
|
||||||
self._session_instructions = content
|
|
||||||
elif isinstance(content, list):
|
|
||||||
self._session_instructions = content[0].get("text")
|
|
||||||
if not messages:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# If we have just a single "user" item, we can just send it normally
|
|
||||||
if len(messages) == 1 and messages[0].get("role") == "user":
|
|
||||||
return [self.from_standard_message(messages[0])]
|
|
||||||
|
|
||||||
# Otherwise, let's pack everything into a single "user" message with a bit of
|
|
||||||
# explanation for the LLM
|
|
||||||
intro_text = """
|
|
||||||
This is a previously saved conversation. Please treat this conversation history as a
|
|
||||||
starting point for the current conversation."""
|
|
||||||
|
|
||||||
trailing_text = """
|
|
||||||
This is the end of the previously saved conversation. Please continue the conversation
|
|
||||||
from here. If the last message is a user instruction or question, act on that instruction
|
|
||||||
or answer the question. If the last message is an assistant response, simple say that you
|
|
||||||
are ready to continue the conversation."""
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"type": "message",
|
|
||||||
"content": [
|
|
||||||
{
|
|
||||||
"type": "input_text",
|
|
||||||
"text": "\n\n".join(
|
|
||||||
[intro_text, json.dumps(messages, indent=2), trailing_text]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
def add_user_content_item_as_message(self, item):
|
|
||||||
"""Add a user content item as a standard message to the context.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
item: The conversation item to add as a user message.
|
|
||||||
"""
|
|
||||||
message = {
|
|
||||||
"role": "user",
|
|
||||||
"content": [{"type": "text", "text": item.content[0].transcript}],
|
|
||||||
}
|
|
||||||
self.add_message(message)
|
|
||||||
|
|
||||||
|
|
||||||
class OpenAIRealtimeUserContextAggregator(OpenAIUserContextAggregator):
|
|
||||||
"""User context aggregator for OpenAI Realtime API.
|
|
||||||
|
|
||||||
Handles user input frames and generates appropriate context updates
|
|
||||||
for the realtime conversation, including message updates and tool settings.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context: The OpenAI realtime LLM context.
|
|
||||||
**kwargs: Additional arguments passed to parent aggregator.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def process_frame(
|
|
||||||
self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM
|
|
||||||
):
|
|
||||||
"""Process incoming frames and handle realtime-specific frame types.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame: The frame to process.
|
|
||||||
direction: The direction of frame flow in the pipeline.
|
|
||||||
"""
|
|
||||||
await super().process_frame(frame, direction)
|
|
||||||
# Parent does not push LLMMessagesUpdateFrame. This ensures that in a typical pipeline,
|
|
||||||
# messages are only processed by the user context aggregator, which is generally what we want. But
|
|
||||||
# we also need to send new messages over the websocket, so the openai realtime API has them
|
|
||||||
# in its context.
|
|
||||||
if isinstance(frame, LLMMessagesUpdateFrame):
|
|
||||||
await self.push_frame(RealtimeMessagesUpdateFrame(context=self._context))
|
|
||||||
|
|
||||||
# Parent also doesn't push the LLMSetToolsFrame.
|
|
||||||
if isinstance(frame, LLMSetToolsFrame):
|
|
||||||
await self.push_frame(frame, direction)
|
|
||||||
|
|
||||||
async def push_aggregation(self):
|
|
||||||
"""Push user input aggregation.
|
|
||||||
|
|
||||||
Currently ignores all user input coming into the pipeline as realtime
|
|
||||||
audio input is handled directly by the service.
|
|
||||||
"""
|
|
||||||
# for the moment, ignore all user input coming into the pipeline.
|
|
||||||
# todo: think about whether/how to fix this to allow for text input from
|
|
||||||
# upstream (transport/transcription, or other sources)
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OpenAIRealtimeAssistantContextAggregator(OpenAIAssistantContextAggregator):
|
|
||||||
"""Assistant context aggregator for OpenAI Realtime API.
|
|
||||||
|
|
||||||
Handles assistant output frames from the realtime service, filtering
|
|
||||||
out duplicate text frames and managing function call results.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context: The OpenAI realtime LLM context.
|
|
||||||
**kwargs: Additional arguments passed to parent aggregator.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# The LLMAssistantContextAggregator uses TextFrames to aggregate the LLM output,
|
|
||||||
# but the OpenAIRealtimeLLMService pushes LLMTextFrames and TTSTextFrames. We
|
|
||||||
# need to override this proces_frame for LLMTextFrame, so that only the TTSTextFrames
|
|
||||||
# are process. This ensures that the context gets only one set of messages.
|
|
||||||
# OpenAIRealtimeLLMService also pushes TranscriptionFrames and InterimTranscriptionFrames,
|
|
||||||
# so we need to ignore pushing those as well, as they're also TextFrames.
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
||||||
"""Process assistant frames, filtering out duplicate text content.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame: The frame to process.
|
|
||||||
direction: The direction of frame flow in the pipeline.
|
|
||||||
"""
|
|
||||||
if not isinstance(frame, (LLMTextFrame, TranscriptionFrame, InterimTranscriptionFrame)):
|
|
||||||
await super().process_frame(frame, direction)
|
|
||||||
|
|
||||||
async def handle_function_call_result(self, frame: FunctionCallResultFrame):
|
|
||||||
"""Handle function call result and notify the realtime service.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame: The function call result frame to handle.
|
|
||||||
"""
|
|
||||||
await super().handle_function_call_result(frame)
|
|
||||||
|
|
||||||
# The standard function callback code path pushes the FunctionCallResultFrame from the llm itself,
|
|
||||||
# so we didn't have a chance to add the result to the openai realtime api context. Let's push a
|
|
||||||
# special frame to do that.
|
|
||||||
await self.push_frame(
|
|
||||||
RealtimeFunctionCallResultFrame(result_frame=frame), FrameDirection.UPSTREAM
|
|
||||||
)
|
|
||||||
@@ -1,978 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Event models and data structures for OpenAI Realtime API communication."""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import uuid
|
|
||||||
from typing import Any, Dict, List, Literal, Optional, Union
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
|
||||||
|
|
||||||
#
|
|
||||||
# session properties
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class InputAudioTranscription(BaseModel):
|
|
||||||
"""Configuration for audio transcription settings."""
|
|
||||||
|
|
||||||
model: str = "gpt-4o-transcribe"
|
|
||||||
language: Optional[str]
|
|
||||||
prompt: Optional[str]
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
model: Optional[str] = "gpt-4o-transcribe",
|
|
||||||
language: Optional[str] = None,
|
|
||||||
prompt: Optional[str] = None,
|
|
||||||
):
|
|
||||||
"""Initialize InputAudioTranscription.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
model: Transcription model to use (e.g., "gpt-4o-transcribe", "whisper-1").
|
|
||||||
language: Optional language code for transcription.
|
|
||||||
prompt: Optional transcription hint text.
|
|
||||||
"""
|
|
||||||
super().__init__(model=model, language=language, prompt=prompt)
|
|
||||||
|
|
||||||
|
|
||||||
class TurnDetection(BaseModel):
|
|
||||||
"""Server-side voice activity detection configuration.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Detection type, must be "server_vad".
|
|
||||||
threshold: Voice activity detection threshold (0.0-1.0). Defaults to 0.5.
|
|
||||||
prefix_padding_ms: Padding before speech starts in milliseconds. Defaults to 300.
|
|
||||||
silence_duration_ms: Silence duration to detect speech end in milliseconds. Defaults to 800.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Optional[Literal["server_vad"]] = "server_vad"
|
|
||||||
threshold: Optional[float] = 0.5
|
|
||||||
prefix_padding_ms: Optional[int] = 300
|
|
||||||
silence_duration_ms: Optional[int] = 800
|
|
||||||
|
|
||||||
|
|
||||||
class SemanticTurnDetection(BaseModel):
|
|
||||||
"""Semantic-based turn detection configuration.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Detection type, must be "semantic_vad".
|
|
||||||
eagerness: Turn detection eagerness level. Can be "low", "medium", "high", or "auto".
|
|
||||||
create_response: Whether to automatically create responses on turn detection.
|
|
||||||
interrupt_response: Whether to interrupt ongoing responses on turn detection.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Optional[Literal["semantic_vad"]] = "semantic_vad"
|
|
||||||
eagerness: Optional[Literal["low", "medium", "high", "auto"]] = None
|
|
||||||
create_response: Optional[bool] = None
|
|
||||||
interrupt_response: Optional[bool] = None
|
|
||||||
|
|
||||||
|
|
||||||
class InputAudioNoiseReduction(BaseModel):
|
|
||||||
"""Input audio noise reduction configuration.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Noise reduction type for different microphone scenarios.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Optional[Literal["near_field", "far_field"]]
|
|
||||||
|
|
||||||
|
|
||||||
class SessionProperties(BaseModel):
|
|
||||||
"""Configuration properties for an OpenAI Realtime session.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
modalities: Communication modalities to enable (text, audio, or both).
|
|
||||||
instructions: System instructions for the assistant.
|
|
||||||
voice: Voice ID for text-to-speech output.
|
|
||||||
input_audio_format: Format for input audio data.
|
|
||||||
output_audio_format: Format for output audio data.
|
|
||||||
input_audio_transcription: Configuration for input audio transcription.
|
|
||||||
input_audio_noise_reduction: Configuration for input audio noise reduction.
|
|
||||||
turn_detection: Turn detection configuration or False to disable.
|
|
||||||
tools: Available function tools for the assistant.
|
|
||||||
tool_choice: Tool usage strategy ("auto", "none", or "required").
|
|
||||||
temperature: Sampling temperature for response generation.
|
|
||||||
max_response_output_tokens: Maximum tokens in response or "inf" for unlimited.
|
|
||||||
"""
|
|
||||||
|
|
||||||
modalities: Optional[List[Literal["text", "audio"]]] = None
|
|
||||||
instructions: Optional[str] = None
|
|
||||||
voice: Optional[str] = None
|
|
||||||
input_audio_format: Optional[Literal["pcm16", "g711_ulaw", "g711_alaw"]] = None
|
|
||||||
output_audio_format: Optional[Literal["pcm16", "g711_ulaw", "g711_alaw"]] = None
|
|
||||||
input_audio_transcription: Optional[InputAudioTranscription] = None
|
|
||||||
input_audio_noise_reduction: Optional[InputAudioNoiseReduction] = None
|
|
||||||
# set turn_detection to False to disable turn detection
|
|
||||||
turn_detection: Optional[Union[TurnDetection, SemanticTurnDetection, bool]] = Field(
|
|
||||||
default=None
|
|
||||||
)
|
|
||||||
tools: Optional[List[Dict]] = None
|
|
||||||
tool_choice: Optional[Literal["auto", "none", "required"]] = None
|
|
||||||
temperature: Optional[float] = None
|
|
||||||
max_response_output_tokens: Optional[Union[int, Literal["inf"]]] = None
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# context
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class ItemContent(BaseModel):
|
|
||||||
"""Content within a conversation item.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Content type (text, audio, input_text, or input_audio).
|
|
||||||
text: Text content for text-based items.
|
|
||||||
audio: Base64-encoded audio data for audio items.
|
|
||||||
transcript: Transcribed text for audio items.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["text", "audio", "input_text", "input_audio"]
|
|
||||||
text: Optional[str] = None
|
|
||||||
audio: Optional[str] = None # base64-encoded audio
|
|
||||||
transcript: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationItem(BaseModel):
|
|
||||||
"""A conversation item in the realtime session.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
id: Unique identifier for the item, auto-generated if not provided.
|
|
||||||
object: Object type identifier for the realtime API.
|
|
||||||
type: Item type (message, function_call, or function_call_output).
|
|
||||||
status: Current status of the item.
|
|
||||||
role: Speaker role for message items (user, assistant, or system).
|
|
||||||
content: Content list for message items.
|
|
||||||
call_id: Function call identifier for function_call items.
|
|
||||||
name: Function name for function_call items.
|
|
||||||
arguments: Function arguments as JSON string for function_call items.
|
|
||||||
output: Function output as JSON string for function_call_output items.
|
|
||||||
"""
|
|
||||||
|
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4().hex))
|
|
||||||
object: Optional[Literal["realtime.item"]] = None
|
|
||||||
type: Literal["message", "function_call", "function_call_output"]
|
|
||||||
status: Optional[Literal["completed", "in_progress", "incomplete"]] = None
|
|
||||||
# role and content are present for message items
|
|
||||||
role: Optional[Literal["user", "assistant", "system"]] = None
|
|
||||||
content: Optional[List[ItemContent]] = None
|
|
||||||
# these four fields are present for function_call items
|
|
||||||
call_id: Optional[str] = None
|
|
||||||
name: Optional[str] = None
|
|
||||||
arguments: Optional[str] = None
|
|
||||||
output: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class RealtimeConversation(BaseModel):
|
|
||||||
"""A realtime conversation session.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
id: Unique identifier for the conversation.
|
|
||||||
object: Object type identifier, always "realtime.conversation".
|
|
||||||
"""
|
|
||||||
|
|
||||||
id: str
|
|
||||||
object: Literal["realtime.conversation"]
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseProperties(BaseModel):
|
|
||||||
"""Properties for configuring assistant responses.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
modalities: Output modalities for the response. Defaults to ["audio", "text"].
|
|
||||||
instructions: Specific instructions for this response.
|
|
||||||
voice: Voice ID for text-to-speech in this response.
|
|
||||||
output_audio_format: Audio format for this response.
|
|
||||||
tools: Available tools for this response.
|
|
||||||
tool_choice: Tool usage strategy for this response.
|
|
||||||
temperature: Sampling temperature for this response.
|
|
||||||
max_response_output_tokens: Maximum tokens for this response.
|
|
||||||
"""
|
|
||||||
|
|
||||||
modalities: Optional[List[Literal["text", "audio"]]] = ["audio", "text"]
|
|
||||||
instructions: Optional[str] = None
|
|
||||||
voice: Optional[str] = None
|
|
||||||
output_audio_format: Optional[Literal["pcm16", "g711_ulaw", "g711_alaw"]] = None
|
|
||||||
tools: Optional[List[Dict]] = Field(default_factory=list)
|
|
||||||
tool_choice: Optional[Literal["auto", "none", "required"]] = None
|
|
||||||
temperature: Optional[float] = None
|
|
||||||
max_response_output_tokens: Optional[Union[int, Literal["inf"]]] = None
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# error class
|
|
||||||
#
|
|
||||||
class RealtimeError(BaseModel):
|
|
||||||
"""Error information from the realtime API.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Error type identifier.
|
|
||||||
code: Specific error code.
|
|
||||||
message: Human-readable error message.
|
|
||||||
param: Parameter name that caused the error, if applicable.
|
|
||||||
event_id: Event ID associated with the error, if applicable.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: str
|
|
||||||
code: Optional[str] = ""
|
|
||||||
message: str
|
|
||||||
param: Optional[str] = None
|
|
||||||
event_id: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# client events
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class ClientEvent(BaseModel):
|
|
||||||
"""Base class for client events sent to the realtime API.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
event_id: Unique identifier for the event, auto-generated if not provided.
|
|
||||||
"""
|
|
||||||
|
|
||||||
event_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
||||||
|
|
||||||
|
|
||||||
class SessionUpdateEvent(ClientEvent):
|
|
||||||
"""Event to update session properties.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "session.update".
|
|
||||||
session: Updated session properties.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["session.update"] = "session.update"
|
|
||||||
session: SessionProperties
|
|
||||||
|
|
||||||
def model_dump(self, *args, **kwargs) -> Dict[str, Any]:
|
|
||||||
"""Serialize the event to a dictionary.
|
|
||||||
|
|
||||||
Handles special serialization for turn_detection where False becomes null.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
*args: Positional arguments passed to parent model_dump.
|
|
||||||
**kwargs: Keyword arguments passed to parent model_dump.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary representation of the event.
|
|
||||||
"""
|
|
||||||
dump = super().model_dump(*args, **kwargs)
|
|
||||||
|
|
||||||
# Handle turn_detection so that False is serialized as null
|
|
||||||
if "turn_detection" in dump["session"]:
|
|
||||||
if dump["session"]["turn_detection"] is False:
|
|
||||||
dump["session"]["turn_detection"] = None
|
|
||||||
|
|
||||||
return dump
|
|
||||||
|
|
||||||
|
|
||||||
class InputAudioBufferAppendEvent(ClientEvent):
|
|
||||||
"""Event to append audio data to the input buffer.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "input_audio_buffer.append".
|
|
||||||
audio: Base64-encoded audio data to append.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["input_audio_buffer.append"] = "input_audio_buffer.append"
|
|
||||||
audio: str # base64-encoded audio
|
|
||||||
|
|
||||||
|
|
||||||
class InputAudioBufferCommitEvent(ClientEvent):
|
|
||||||
"""Event to commit the current input audio buffer.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "input_audio_buffer.commit".
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["input_audio_buffer.commit"] = "input_audio_buffer.commit"
|
|
||||||
|
|
||||||
|
|
||||||
class InputAudioBufferClearEvent(ClientEvent):
|
|
||||||
"""Event to clear the input audio buffer.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "input_audio_buffer.clear".
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["input_audio_buffer.clear"] = "input_audio_buffer.clear"
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationItemCreateEvent(ClientEvent):
|
|
||||||
"""Event to create a new conversation item.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "conversation.item.create".
|
|
||||||
previous_item_id: ID of the item to insert after, if any.
|
|
||||||
item: The conversation item to create.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["conversation.item.create"] = "conversation.item.create"
|
|
||||||
previous_item_id: Optional[str] = None
|
|
||||||
item: ConversationItem
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationItemTruncateEvent(ClientEvent):
|
|
||||||
"""Event to truncate a conversation item's audio content.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "conversation.item.truncate".
|
|
||||||
item_id: ID of the item to truncate.
|
|
||||||
content_index: Index of the content to truncate within the item.
|
|
||||||
audio_end_ms: End time in milliseconds for the truncated audio.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["conversation.item.truncate"] = "conversation.item.truncate"
|
|
||||||
item_id: str
|
|
||||||
content_index: int
|
|
||||||
audio_end_ms: int
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationItemDeleteEvent(ClientEvent):
|
|
||||||
"""Event to delete a conversation item.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "conversation.item.delete".
|
|
||||||
item_id: ID of the item to delete.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["conversation.item.delete"] = "conversation.item.delete"
|
|
||||||
item_id: str
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationItemRetrieveEvent(ClientEvent):
|
|
||||||
"""Event to retrieve a conversation item by ID.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "conversation.item.retrieve".
|
|
||||||
item_id: ID of the item to retrieve.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["conversation.item.retrieve"] = "conversation.item.retrieve"
|
|
||||||
item_id: str
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseCreateEvent(ClientEvent):
|
|
||||||
"""Event to create a new assistant response.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.create".
|
|
||||||
response: Optional response configuration properties.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.create"] = "response.create"
|
|
||||||
response: Optional[ResponseProperties] = None
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseCancelEvent(ClientEvent):
|
|
||||||
"""Event to cancel the current assistant response.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.cancel".
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.cancel"] = "response.cancel"
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# server events
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class ServerEvent(BaseModel):
|
|
||||||
"""Base class for server events received from the realtime API.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
event_id: Unique identifier for the event.
|
|
||||||
type: Type of the server event.
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
||||||
|
|
||||||
event_id: str
|
|
||||||
type: str
|
|
||||||
|
|
||||||
|
|
||||||
class SessionCreatedEvent(ServerEvent):
|
|
||||||
"""Event indicating a session has been created.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "session.created".
|
|
||||||
session: The created session properties.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["session.created"]
|
|
||||||
session: SessionProperties
|
|
||||||
|
|
||||||
|
|
||||||
class SessionUpdatedEvent(ServerEvent):
|
|
||||||
"""Event indicating a session has been updated.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "session.updated".
|
|
||||||
session: The updated session properties.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["session.updated"]
|
|
||||||
session: SessionProperties
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationCreated(ServerEvent):
|
|
||||||
"""Event indicating a conversation has been created.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "conversation.created".
|
|
||||||
conversation: The created conversation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["conversation.created"]
|
|
||||||
conversation: RealtimeConversation
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationItemCreated(ServerEvent):
|
|
||||||
"""Event indicating a conversation item has been created.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "conversation.item.created".
|
|
||||||
previous_item_id: ID of the previous item, if any.
|
|
||||||
item: The created conversation item.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["conversation.item.created"]
|
|
||||||
previous_item_id: Optional[str] = None
|
|
||||||
item: ConversationItem
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationItemInputAudioTranscriptionDelta(ServerEvent):
|
|
||||||
"""Event containing incremental input audio transcription.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "conversation.item.input_audio_transcription.delta".
|
|
||||||
item_id: ID of the conversation item being transcribed.
|
|
||||||
content_index: Index of the content within the item.
|
|
||||||
delta: Incremental transcription text.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["conversation.item.input_audio_transcription.delta"]
|
|
||||||
item_id: str
|
|
||||||
content_index: int
|
|
||||||
delta: str
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationItemInputAudioTranscriptionCompleted(ServerEvent):
|
|
||||||
"""Event indicating input audio transcription is complete.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "conversation.item.input_audio_transcription.completed".
|
|
||||||
item_id: ID of the conversation item that was transcribed.
|
|
||||||
content_index: Index of the content within the item.
|
|
||||||
transcript: Complete transcription text.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["conversation.item.input_audio_transcription.completed"]
|
|
||||||
item_id: str
|
|
||||||
content_index: int
|
|
||||||
transcript: str
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationItemInputAudioTranscriptionFailed(ServerEvent):
|
|
||||||
"""Event indicating input audio transcription failed.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "conversation.item.input_audio_transcription.failed".
|
|
||||||
item_id: ID of the conversation item that failed transcription.
|
|
||||||
content_index: Index of the content within the item.
|
|
||||||
error: Error details for the transcription failure.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["conversation.item.input_audio_transcription.failed"]
|
|
||||||
item_id: str
|
|
||||||
content_index: int
|
|
||||||
error: RealtimeError
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationItemTruncated(ServerEvent):
|
|
||||||
"""Event indicating a conversation item has been truncated.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "conversation.item.truncated".
|
|
||||||
item_id: ID of the truncated conversation item.
|
|
||||||
content_index: Index of the content within the item.
|
|
||||||
audio_end_ms: End time in milliseconds for the truncated audio.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["conversation.item.truncated"]
|
|
||||||
item_id: str
|
|
||||||
content_index: int
|
|
||||||
audio_end_ms: int
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationItemDeleted(ServerEvent):
|
|
||||||
"""Event indicating a conversation item has been deleted.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "conversation.item.deleted".
|
|
||||||
item_id: ID of the deleted conversation item.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["conversation.item.deleted"]
|
|
||||||
item_id: str
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationItemRetrieved(ServerEvent):
|
|
||||||
"""Event containing a retrieved conversation item.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "conversation.item.retrieved".
|
|
||||||
item: The retrieved conversation item.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["conversation.item.retrieved"]
|
|
||||||
item: ConversationItem
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseCreated(ServerEvent):
|
|
||||||
"""Event indicating an assistant response has been created.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.created".
|
|
||||||
response: The created response object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.created"]
|
|
||||||
response: "Response"
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseDone(ServerEvent):
|
|
||||||
"""Event indicating an assistant response is complete.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.done".
|
|
||||||
response: The completed response object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.done"]
|
|
||||||
response: "Response"
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseOutputItemAdded(ServerEvent):
|
|
||||||
"""Event indicating an output item has been added to a response.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.output_item.added".
|
|
||||||
response_id: ID of the response.
|
|
||||||
output_index: Index of the output item.
|
|
||||||
item: The added conversation item.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.output_item.added"]
|
|
||||||
response_id: str
|
|
||||||
output_index: int
|
|
||||||
item: ConversationItem
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseOutputItemDone(ServerEvent):
|
|
||||||
"""Event indicating an output item is complete.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.output_item.done".
|
|
||||||
response_id: ID of the response.
|
|
||||||
output_index: Index of the output item.
|
|
||||||
item: The completed conversation item.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.output_item.done"]
|
|
||||||
response_id: str
|
|
||||||
output_index: int
|
|
||||||
item: ConversationItem
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseContentPartAdded(ServerEvent):
|
|
||||||
"""Event indicating a content part has been added to a response.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.content_part.added".
|
|
||||||
response_id: ID of the response.
|
|
||||||
item_id: ID of the conversation item.
|
|
||||||
output_index: Index of the output item.
|
|
||||||
content_index: Index of the content part.
|
|
||||||
part: The added content part.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.content_part.added"]
|
|
||||||
response_id: str
|
|
||||||
item_id: str
|
|
||||||
output_index: int
|
|
||||||
content_index: int
|
|
||||||
part: ItemContent
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseContentPartDone(ServerEvent):
|
|
||||||
"""Event indicating a content part is complete.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.content_part.done".
|
|
||||||
response_id: ID of the response.
|
|
||||||
item_id: ID of the conversation item.
|
|
||||||
output_index: Index of the output item.
|
|
||||||
content_index: Index of the content part.
|
|
||||||
part: The completed content part.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.content_part.done"]
|
|
||||||
response_id: str
|
|
||||||
item_id: str
|
|
||||||
output_index: int
|
|
||||||
content_index: int
|
|
||||||
part: ItemContent
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseTextDelta(ServerEvent):
|
|
||||||
"""Event containing incremental text from a response.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.text.delta".
|
|
||||||
response_id: ID of the response.
|
|
||||||
item_id: ID of the conversation item.
|
|
||||||
output_index: Index of the output item.
|
|
||||||
content_index: Index of the content part.
|
|
||||||
delta: Incremental text content.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.text.delta"]
|
|
||||||
response_id: str
|
|
||||||
item_id: str
|
|
||||||
output_index: int
|
|
||||||
content_index: int
|
|
||||||
delta: str
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseTextDone(ServerEvent):
|
|
||||||
"""Event indicating text content is complete.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.text.done".
|
|
||||||
response_id: ID of the response.
|
|
||||||
item_id: ID of the conversation item.
|
|
||||||
output_index: Index of the output item.
|
|
||||||
content_index: Index of the content part.
|
|
||||||
text: Complete text content.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.text.done"]
|
|
||||||
response_id: str
|
|
||||||
item_id: str
|
|
||||||
output_index: int
|
|
||||||
content_index: int
|
|
||||||
text: str
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseAudioTranscriptDelta(ServerEvent):
|
|
||||||
"""Event containing incremental audio transcript from a response.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.audio_transcript.delta".
|
|
||||||
response_id: ID of the response.
|
|
||||||
item_id: ID of the conversation item.
|
|
||||||
output_index: Index of the output item.
|
|
||||||
content_index: Index of the content part.
|
|
||||||
delta: Incremental transcript text.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.audio_transcript.delta"]
|
|
||||||
response_id: str
|
|
||||||
item_id: str
|
|
||||||
output_index: int
|
|
||||||
content_index: int
|
|
||||||
delta: str
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseAudioTranscriptDone(ServerEvent):
|
|
||||||
"""Event indicating audio transcript is complete.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.audio_transcript.done".
|
|
||||||
response_id: ID of the response.
|
|
||||||
item_id: ID of the conversation item.
|
|
||||||
output_index: Index of the output item.
|
|
||||||
content_index: Index of the content part.
|
|
||||||
transcript: Complete transcript text.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.audio_transcript.done"]
|
|
||||||
response_id: str
|
|
||||||
item_id: str
|
|
||||||
output_index: int
|
|
||||||
content_index: int
|
|
||||||
transcript: str
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseAudioDelta(ServerEvent):
|
|
||||||
"""Event containing incremental audio data from a response.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.audio.delta".
|
|
||||||
response_id: ID of the response.
|
|
||||||
item_id: ID of the conversation item.
|
|
||||||
output_index: Index of the output item.
|
|
||||||
content_index: Index of the content part.
|
|
||||||
delta: Base64-encoded incremental audio data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.audio.delta"]
|
|
||||||
response_id: str
|
|
||||||
item_id: str
|
|
||||||
output_index: int
|
|
||||||
content_index: int
|
|
||||||
delta: str # base64-encoded audio
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseAudioDone(ServerEvent):
|
|
||||||
"""Event indicating audio content is complete.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.audio.done".
|
|
||||||
response_id: ID of the response.
|
|
||||||
item_id: ID of the conversation item.
|
|
||||||
output_index: Index of the output item.
|
|
||||||
content_index: Index of the content part.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.audio.done"]
|
|
||||||
response_id: str
|
|
||||||
item_id: str
|
|
||||||
output_index: int
|
|
||||||
content_index: int
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseFunctionCallArgumentsDelta(ServerEvent):
|
|
||||||
"""Event containing incremental function call arguments.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.function_call_arguments.delta".
|
|
||||||
response_id: ID of the response.
|
|
||||||
item_id: ID of the conversation item.
|
|
||||||
output_index: Index of the output item.
|
|
||||||
call_id: ID of the function call.
|
|
||||||
delta: Incremental function arguments as JSON.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.function_call_arguments.delta"]
|
|
||||||
response_id: str
|
|
||||||
item_id: str
|
|
||||||
output_index: int
|
|
||||||
call_id: str
|
|
||||||
delta: str
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseFunctionCallArgumentsDone(ServerEvent):
|
|
||||||
"""Event indicating function call arguments are complete.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "response.function_call_arguments.done".
|
|
||||||
response_id: ID of the response.
|
|
||||||
item_id: ID of the conversation item.
|
|
||||||
output_index: Index of the output item.
|
|
||||||
call_id: ID of the function call.
|
|
||||||
arguments: Complete function arguments as JSON string.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["response.function_call_arguments.done"]
|
|
||||||
response_id: str
|
|
||||||
item_id: str
|
|
||||||
output_index: int
|
|
||||||
call_id: str
|
|
||||||
arguments: str
|
|
||||||
|
|
||||||
|
|
||||||
class InputAudioBufferSpeechStarted(ServerEvent):
|
|
||||||
"""Event indicating speech has started in the input audio buffer.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "input_audio_buffer.speech_started".
|
|
||||||
audio_start_ms: Start time of speech in milliseconds.
|
|
||||||
item_id: ID of the associated conversation item.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["input_audio_buffer.speech_started"]
|
|
||||||
audio_start_ms: int
|
|
||||||
item_id: str
|
|
||||||
|
|
||||||
|
|
||||||
class InputAudioBufferSpeechStopped(ServerEvent):
|
|
||||||
"""Event indicating speech has stopped in the input audio buffer.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "input_audio_buffer.speech_stopped".
|
|
||||||
audio_end_ms: End time of speech in milliseconds.
|
|
||||||
item_id: ID of the associated conversation item.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["input_audio_buffer.speech_stopped"]
|
|
||||||
audio_end_ms: int
|
|
||||||
item_id: str
|
|
||||||
|
|
||||||
|
|
||||||
class InputAudioBufferCommitted(ServerEvent):
|
|
||||||
"""Event indicating the input audio buffer has been committed.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "input_audio_buffer.committed".
|
|
||||||
previous_item_id: ID of the previous item, if any.
|
|
||||||
item_id: ID of the committed conversation item.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["input_audio_buffer.committed"]
|
|
||||||
previous_item_id: Optional[str] = None
|
|
||||||
item_id: str
|
|
||||||
|
|
||||||
|
|
||||||
class InputAudioBufferCleared(ServerEvent):
|
|
||||||
"""Event indicating the input audio buffer has been cleared.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "input_audio_buffer.cleared".
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["input_audio_buffer.cleared"]
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorEvent(ServerEvent):
|
|
||||||
"""Event indicating an error occurred.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "error".
|
|
||||||
error: Error details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["error"]
|
|
||||||
error: RealtimeError
|
|
||||||
|
|
||||||
|
|
||||||
class RateLimitsUpdated(ServerEvent):
|
|
||||||
"""Event indicating rate limits have been updated.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
type: Event type, always "rate_limits.updated".
|
|
||||||
rate_limits: List of rate limit information.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: Literal["rate_limits.updated"]
|
|
||||||
rate_limits: List[Dict[str, Any]]
|
|
||||||
|
|
||||||
|
|
||||||
class TokenDetails(BaseModel):
|
|
||||||
"""Detailed token usage information.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
cached_tokens: Number of cached tokens used. Defaults to 0.
|
|
||||||
text_tokens: Number of text tokens used. Defaults to 0.
|
|
||||||
audio_tokens: Number of audio tokens used. Defaults to 0.
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_config = ConfigDict(extra="allow")
|
|
||||||
|
|
||||||
cached_tokens: Optional[int] = 0
|
|
||||||
text_tokens: Optional[int] = 0
|
|
||||||
audio_tokens: Optional[int] = 0
|
|
||||||
|
|
||||||
|
|
||||||
class Usage(BaseModel):
|
|
||||||
"""Token usage statistics for a response.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
total_tokens: Total number of tokens used.
|
|
||||||
input_tokens: Number of input tokens used.
|
|
||||||
output_tokens: Number of output tokens used.
|
|
||||||
input_token_details: Detailed breakdown of input token usage.
|
|
||||||
output_token_details: Detailed breakdown of output token usage.
|
|
||||||
"""
|
|
||||||
|
|
||||||
total_tokens: int
|
|
||||||
input_tokens: int
|
|
||||||
output_tokens: int
|
|
||||||
input_token_details: TokenDetails
|
|
||||||
output_token_details: TokenDetails
|
|
||||||
|
|
||||||
|
|
||||||
class Response(BaseModel):
|
|
||||||
"""A complete assistant response.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
id: Unique identifier for the response.
|
|
||||||
object: Object type, always "realtime.response".
|
|
||||||
status: Current status of the response.
|
|
||||||
status_details: Additional status information.
|
|
||||||
output: List of conversation items in the response.
|
|
||||||
usage: Token usage statistics for the response.
|
|
||||||
"""
|
|
||||||
|
|
||||||
id: str
|
|
||||||
object: Literal["realtime.response"]
|
|
||||||
status: Literal["completed", "in_progress", "incomplete", "cancelled", "failed"]
|
|
||||||
status_details: Any
|
|
||||||
output: List[ConversationItem]
|
|
||||||
usage: Optional[Usage] = None
|
|
||||||
|
|
||||||
|
|
||||||
_server_event_types = {
|
|
||||||
"error": ErrorEvent,
|
|
||||||
"session.created": SessionCreatedEvent,
|
|
||||||
"session.updated": SessionUpdatedEvent,
|
|
||||||
"conversation.created": ConversationCreated,
|
|
||||||
"input_audio_buffer.committed": InputAudioBufferCommitted,
|
|
||||||
"input_audio_buffer.cleared": InputAudioBufferCleared,
|
|
||||||
"input_audio_buffer.speech_started": InputAudioBufferSpeechStarted,
|
|
||||||
"input_audio_buffer.speech_stopped": InputAudioBufferSpeechStopped,
|
|
||||||
"conversation.item.created": ConversationItemCreated,
|
|
||||||
"conversation.item.input_audio_transcription.delta": ConversationItemInputAudioTranscriptionDelta,
|
|
||||||
"conversation.item.input_audio_transcription.completed": ConversationItemInputAudioTranscriptionCompleted,
|
|
||||||
"conversation.item.input_audio_transcription.failed": ConversationItemInputAudioTranscriptionFailed,
|
|
||||||
"conversation.item.truncated": ConversationItemTruncated,
|
|
||||||
"conversation.item.deleted": ConversationItemDeleted,
|
|
||||||
"conversation.item.retrieved": ConversationItemRetrieved,
|
|
||||||
"response.created": ResponseCreated,
|
|
||||||
"response.done": ResponseDone,
|
|
||||||
"response.output_item.added": ResponseOutputItemAdded,
|
|
||||||
"response.output_item.done": ResponseOutputItemDone,
|
|
||||||
"response.content_part.added": ResponseContentPartAdded,
|
|
||||||
"response.content_part.done": ResponseContentPartDone,
|
|
||||||
"response.text.delta": ResponseTextDelta,
|
|
||||||
"response.text.done": ResponseTextDone,
|
|
||||||
"response.audio_transcript.delta": ResponseAudioTranscriptDelta,
|
|
||||||
"response.audio_transcript.done": ResponseAudioTranscriptDone,
|
|
||||||
"response.audio.delta": ResponseAudioDelta,
|
|
||||||
"response.audio.done": ResponseAudioDone,
|
|
||||||
"response.function_call_arguments.delta": ResponseFunctionCallArgumentsDelta,
|
|
||||||
"response.function_call_arguments.done": ResponseFunctionCallArgumentsDone,
|
|
||||||
"rate_limits.updated": RateLimitsUpdated,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def parse_server_event(str):
|
|
||||||
"""Parse a server event from JSON string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
str: JSON string containing the server event.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Parsed server event object of the appropriate type.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Exception: If the event type is unimplemented or parsing fails.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
event = json.loads(str)
|
|
||||||
event_type = event["type"]
|
|
||||||
if event_type not in _server_event_types:
|
|
||||||
raise Exception(f"Unimplemented server event type: {event_type}")
|
|
||||||
return _server_event_types[event_type].model_validate(event)
|
|
||||||
except Exception as e:
|
|
||||||
raise Exception(f"{e} \n\n{str}")
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Custom frame types for OpenAI Realtime API integration."""
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from pipecat.frames.frames import DataFrame, FunctionCallResultFrame
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from pipecat.services.openai_realtime_beta.context import OpenAIRealtimeLLMContext
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RealtimeMessagesUpdateFrame(DataFrame):
|
|
||||||
"""Frame indicating that the realtime context messages have been updated.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
context: The updated OpenAI realtime LLM context.
|
|
||||||
"""
|
|
||||||
|
|
||||||
context: "OpenAIRealtimeLLMContext"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RealtimeFunctionCallResultFrame(DataFrame):
|
|
||||||
"""Frame containing function call results for the realtime service.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
result_frame: The function call result frame to send to the realtime API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
result_frame: FunctionCallResultFrame
|
|
||||||
@@ -1,858 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""OpenAI Realtime Beta LLM service implementation with WebSocket support."""
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import warnings
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from pipecat.adapters.services.open_ai_realtime_adapter import OpenAIRealtimeLLMAdapter
|
|
||||||
from pipecat.frames.frames import (
|
|
||||||
AggregationType,
|
|
||||||
BotStoppedSpeakingFrame,
|
|
||||||
CancelFrame,
|
|
||||||
EndFrame,
|
|
||||||
ErrorFrame,
|
|
||||||
Frame,
|
|
||||||
InputAudioRawFrame,
|
|
||||||
InterimTranscriptionFrame,
|
|
||||||
InterruptionFrame,
|
|
||||||
LLMContextFrame,
|
|
||||||
LLMFullResponseEndFrame,
|
|
||||||
LLMFullResponseStartFrame,
|
|
||||||
LLMMessagesAppendFrame,
|
|
||||||
LLMSetToolsFrame,
|
|
||||||
LLMTextFrame,
|
|
||||||
LLMUpdateSettingsFrame,
|
|
||||||
StartFrame,
|
|
||||||
TranscriptionFrame,
|
|
||||||
TTSAudioRawFrame,
|
|
||||||
TTSStartedFrame,
|
|
||||||
TTSStoppedFrame,
|
|
||||||
TTSTextFrame,
|
|
||||||
UserStartedSpeakingFrame,
|
|
||||||
UserStoppedSpeakingFrame,
|
|
||||||
)
|
|
||||||
from pipecat.metrics.metrics import LLMTokenUsage
|
|
||||||
from pipecat.processors.aggregators.llm_response import (
|
|
||||||
LLMAssistantAggregatorParams,
|
|
||||||
LLMUserAggregatorParams,
|
|
||||||
)
|
|
||||||
from pipecat.processors.aggregators.openai_llm_context import (
|
|
||||||
OpenAILLMContext,
|
|
||||||
OpenAILLMContextFrame,
|
|
||||||
)
|
|
||||||
from pipecat.processors.frame_processor import FrameDirection
|
|
||||||
from pipecat.services.llm_service import FunctionCallFromLLM, LLMService
|
|
||||||
from pipecat.services.openai.llm import OpenAIContextAggregatorPair
|
|
||||||
from pipecat.services.settings import LLMSettings
|
|
||||||
from pipecat.transcriptions.language import Language
|
|
||||||
from pipecat.utils.time import time_now_iso8601
|
|
||||||
from pipecat.utils.tracing.service_decorators import traced_openai_realtime, traced_stt
|
|
||||||
|
|
||||||
from . import events
|
|
||||||
from .context import (
|
|
||||||
OpenAIRealtimeAssistantContextAggregator,
|
|
||||||
OpenAIRealtimeLLMContext,
|
|
||||||
OpenAIRealtimeUserContextAggregator,
|
|
||||||
)
|
|
||||||
from .frames import RealtimeFunctionCallResultFrame, RealtimeMessagesUpdateFrame
|
|
||||||
|
|
||||||
try:
|
|
||||||
from websockets.asyncio.client import connect as websocket_connect
|
|
||||||
except ModuleNotFoundError as e:
|
|
||||||
logger.error(f"Exception: {e}")
|
|
||||||
logger.error("In order to use OpenAI, you need to `pip install pipecat-ai[openai]`.")
|
|
||||||
raise Exception(f"Missing module: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CurrentAudioResponse:
|
|
||||||
"""Tracks the current audio response from the assistant.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
item_id: Unique identifier for the audio response item.
|
|
||||||
content_index: Index of the audio content within the item.
|
|
||||||
start_time_ms: Timestamp when the audio response started in milliseconds.
|
|
||||||
total_size: Total size of audio data received in bytes. Defaults to 0.
|
|
||||||
"""
|
|
||||||
|
|
||||||
item_id: str
|
|
||||||
content_index: int
|
|
||||||
start_time_ms: int
|
|
||||||
total_size: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class OpenAIRealtimeBetaLLMSettings(LLMSettings):
|
|
||||||
"""Settings for OpenAIRealtimeBetaLLMService."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OpenAIRealtimeBetaLLMService(LLMService):
|
|
||||||
"""OpenAI Realtime Beta LLM service providing real-time audio and text communication.
|
|
||||||
|
|
||||||
.. deprecated:: 0.0.84
|
|
||||||
`OpenAIRealtimeBetaLLMService` is deprecated, use `OpenAIRealtimeLLMService` instead.
|
|
||||||
This class will be removed in version 1.0.0.
|
|
||||||
|
|
||||||
Implements the OpenAI Realtime API Beta with WebSocket communication for low-latency
|
|
||||||
bidirectional audio and text interactions. Supports function calling, conversation
|
|
||||||
management, and real-time transcription.
|
|
||||||
"""
|
|
||||||
|
|
||||||
Settings = OpenAIRealtimeBetaLLMSettings
|
|
||||||
_settings: Settings
|
|
||||||
|
|
||||||
# Overriding the default adapter to use the OpenAIRealtimeLLMAdapter one.
|
|
||||||
adapter_class = OpenAIRealtimeLLMAdapter
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
api_key: str,
|
|
||||||
model: Optional[str] = None,
|
|
||||||
base_url: str = "wss://api.openai.com/v1/realtime",
|
|
||||||
session_properties: Optional[events.SessionProperties] = None,
|
|
||||||
settings: Optional[Settings] = None,
|
|
||||||
start_audio_paused: bool = False,
|
|
||||||
send_transcription_frames: bool = True,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""Initialize the OpenAI Realtime Beta LLM service.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
api_key: OpenAI API key for authentication.
|
|
||||||
model: OpenAI model name.
|
|
||||||
|
|
||||||
.. deprecated:: 0.0.105
|
|
||||||
Use ``settings=OpenAIRealtimeBetaLLMService.Settings(model=...)`` instead.
|
|
||||||
|
|
||||||
base_url: WebSocket base URL for the realtime API.
|
|
||||||
Defaults to "wss://api.openai.com/v1/realtime".
|
|
||||||
session_properties: Configuration properties for the realtime session.
|
|
||||||
If None, uses default SessionProperties.
|
|
||||||
settings: Runtime-updatable settings for this service.
|
|
||||||
start_audio_paused: Whether to start with audio input paused. Defaults to False.
|
|
||||||
send_transcription_frames: Whether to emit transcription frames. Defaults to True.
|
|
||||||
**kwargs: Additional arguments passed to parent LLMService.
|
|
||||||
"""
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
warnings.warn(
|
|
||||||
"OpenAIRealtimeBetaLLMService is deprecated and will be removed in version 1.0.0. "
|
|
||||||
"Use OpenAIRealtimeLLMService instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 1. Initialize default_settings with hardcoded defaults
|
|
||||||
default_settings = self.Settings(
|
|
||||||
model="gpt-4o-realtime-preview-2025-06-03",
|
|
||||||
system_instruction=None,
|
|
||||||
temperature=None,
|
|
||||||
max_tokens=None,
|
|
||||||
top_p=None,
|
|
||||||
top_k=None,
|
|
||||||
frequency_penalty=None,
|
|
||||||
presence_penalty=None,
|
|
||||||
seed=None,
|
|
||||||
filter_incomplete_user_turns=False,
|
|
||||||
user_turn_completion_config=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. Apply direct init arg overrides (deprecated)
|
|
||||||
if model is not None:
|
|
||||||
self._warn_init_param_moved_to_settings("model", "model")
|
|
||||||
default_settings.model = model
|
|
||||||
# 3. Apply settings delta (canonical API, always wins)
|
|
||||||
if settings is not None:
|
|
||||||
default_settings.apply_update(settings)
|
|
||||||
|
|
||||||
full_url = f"{base_url}?model={default_settings.model}"
|
|
||||||
super().__init__(
|
|
||||||
base_url=full_url,
|
|
||||||
settings=default_settings,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.api_key = api_key
|
|
||||||
self.base_url = full_url
|
|
||||||
self._session_properties = session_properties or events.SessionProperties()
|
|
||||||
self._audio_input_paused = start_audio_paused
|
|
||||||
self._send_transcription_frames = send_transcription_frames
|
|
||||||
self._websocket = None
|
|
||||||
self._receive_task = None
|
|
||||||
self._context = None
|
|
||||||
|
|
||||||
self._disconnecting = False
|
|
||||||
self._api_session_ready = False
|
|
||||||
self._run_llm_when_api_session_ready = False
|
|
||||||
|
|
||||||
self._current_assistant_response = None
|
|
||||||
self._current_audio_response = None
|
|
||||||
|
|
||||||
self._messages_added_manually = {}
|
|
||||||
self._user_and_response_message_tuple = None
|
|
||||||
|
|
||||||
self._register_event_handler("on_conversation_item_created")
|
|
||||||
self._register_event_handler("on_conversation_item_updated")
|
|
||||||
self._retrieve_conversation_item_futures = {}
|
|
||||||
|
|
||||||
def can_generate_metrics(self) -> bool:
|
|
||||||
"""Check if the service can generate usage metrics.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if metrics generation is supported.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def set_audio_input_paused(self, paused: bool):
|
|
||||||
"""Set whether audio input is paused.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
paused: True to pause audio input, False to resume.
|
|
||||||
"""
|
|
||||||
self._audio_input_paused = paused
|
|
||||||
|
|
||||||
def _is_modality_enabled(self, modality: str) -> bool:
|
|
||||||
"""Check if a specific modality is enabled, "text" or "audio"."""
|
|
||||||
modalities = self._session_properties.modalities or ["audio", "text"]
|
|
||||||
return modality in modalities
|
|
||||||
|
|
||||||
def _get_enabled_modalities(self) -> list[str]:
|
|
||||||
"""Get the list of enabled modalities."""
|
|
||||||
return self._session_properties.modalities or ["audio", "text"]
|
|
||||||
|
|
||||||
async def retrieve_conversation_item(self, item_id: str):
|
|
||||||
"""Retrieve a conversation item by ID from the server.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
item_id: The ID of the conversation item to retrieve.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The retrieved conversation item.
|
|
||||||
"""
|
|
||||||
future = self.get_event_loop().create_future()
|
|
||||||
retrieval_in_flight = False
|
|
||||||
if not self._retrieve_conversation_item_futures.get(item_id):
|
|
||||||
self._retrieve_conversation_item_futures[item_id] = []
|
|
||||||
else:
|
|
||||||
retrieval_in_flight = True
|
|
||||||
self._retrieve_conversation_item_futures[item_id].append(future)
|
|
||||||
if not retrieval_in_flight:
|
|
||||||
await self.send_client_event(
|
|
||||||
# Set event_id to "rci_{item_id}" so that we can identify an
|
|
||||||
# error later if the retrieval fails. We don't need a UUID
|
|
||||||
# suffix to the event_id because we're ensuring only one
|
|
||||||
# in-flight retrieval per item_id. (Note: "rci" = "retrieve
|
|
||||||
# conversation item")
|
|
||||||
events.ConversationItemRetrieveEvent(item_id=item_id, event_id=f"rci_{item_id}")
|
|
||||||
)
|
|
||||||
return await future
|
|
||||||
|
|
||||||
#
|
|
||||||
# standard AIService frame handling
|
|
||||||
#
|
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
|
||||||
"""Start the service and establish WebSocket connection.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame: The start frame triggering service initialization.
|
|
||||||
"""
|
|
||||||
await super().start(frame)
|
|
||||||
await self._connect()
|
|
||||||
|
|
||||||
async def stop(self, frame: EndFrame):
|
|
||||||
"""Stop the service and close WebSocket connection.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame: The end frame triggering service shutdown.
|
|
||||||
"""
|
|
||||||
await super().stop(frame)
|
|
||||||
await self._disconnect()
|
|
||||||
|
|
||||||
async def cancel(self, frame: CancelFrame):
|
|
||||||
"""Cancel the service and close WebSocket connection.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame: The cancel frame triggering service cancellation.
|
|
||||||
"""
|
|
||||||
await super().cancel(frame)
|
|
||||||
await self._disconnect()
|
|
||||||
|
|
||||||
#
|
|
||||||
# speech and interruption handling
|
|
||||||
#
|
|
||||||
|
|
||||||
async def _handle_interruption(self):
|
|
||||||
# None and False are different. Check for False. None means we're using OpenAI's
|
|
||||||
# built-in turn detection defaults.
|
|
||||||
if self._session_properties.turn_detection is False:
|
|
||||||
await self.send_client_event(events.InputAudioBufferClearEvent())
|
|
||||||
await self.send_client_event(events.ResponseCancelEvent())
|
|
||||||
await self._truncate_current_audio_response()
|
|
||||||
await self.stop_all_metrics()
|
|
||||||
if self._current_assistant_response:
|
|
||||||
await self.push_frame(LLMFullResponseEndFrame())
|
|
||||||
# Only push TTSStoppedFrame if audio modality is enabled
|
|
||||||
if self._is_modality_enabled("audio"):
|
|
||||||
await self.push_frame(TTSStoppedFrame())
|
|
||||||
|
|
||||||
async def _handle_user_started_speaking(self, frame):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def _handle_user_stopped_speaking(self, frame):
|
|
||||||
# None and False are different. Check for False. None means we're using OpenAI's
|
|
||||||
# built-in turn detection defaults.
|
|
||||||
if self._session_properties.turn_detection is False:
|
|
||||||
await self.send_client_event(events.InputAudioBufferCommitEvent())
|
|
||||||
await self.send_client_event(events.ResponseCreateEvent())
|
|
||||||
|
|
||||||
async def _handle_bot_stopped_speaking(self):
|
|
||||||
self._current_audio_response = None
|
|
||||||
|
|
||||||
def _calculate_audio_duration_ms(
|
|
||||||
self, total_bytes: int, sample_rate: int = 24000, bytes_per_sample: int = 2
|
|
||||||
) -> int:
|
|
||||||
"""Calculate audio duration in milliseconds based on PCM audio parameters."""
|
|
||||||
samples = total_bytes / bytes_per_sample
|
|
||||||
duration_seconds = samples / sample_rate
|
|
||||||
return int(duration_seconds * 1000)
|
|
||||||
|
|
||||||
async def _truncate_current_audio_response(self):
|
|
||||||
"""Truncates the current audio response at the appropriate duration.
|
|
||||||
|
|
||||||
Calculates the actual duration of the audio content and truncates at the shorter of
|
|
||||||
either the wall clock time or the actual audio duration to prevent invalid truncation
|
|
||||||
requests.
|
|
||||||
"""
|
|
||||||
if not self._current_audio_response:
|
|
||||||
return
|
|
||||||
|
|
||||||
# if the bot is still speaking, truncate the last message
|
|
||||||
try:
|
|
||||||
current = self._current_audio_response
|
|
||||||
self._current_audio_response = None
|
|
||||||
|
|
||||||
# Calculate actual audio duration instead of using wall clock time
|
|
||||||
audio_duration_ms = self._calculate_audio_duration_ms(current.total_size)
|
|
||||||
|
|
||||||
# Use the shorter of wall clock time or actual audio duration
|
|
||||||
elapsed_ms = int(time.time() * 1000 - current.start_time_ms)
|
|
||||||
truncate_ms = min(elapsed_ms, audio_duration_ms)
|
|
||||||
|
|
||||||
logger.trace(
|
|
||||||
f"Truncating audio: duration={audio_duration_ms}ms, "
|
|
||||||
f"elapsed={elapsed_ms}ms, truncate={truncate_ms}ms"
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.send_client_event(
|
|
||||||
events.ConversationItemTruncateEvent(
|
|
||||||
item_id=current.item_id,
|
|
||||||
content_index=current.content_index,
|
|
||||||
audio_end_ms=truncate_ms,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
# Log warning and don't re-raise - allow session to continue
|
|
||||||
logger.warning(f"Audio truncation failed (non-fatal): {e}")
|
|
||||||
|
|
||||||
#
|
|
||||||
# frame processing
|
|
||||||
#
|
|
||||||
# StartFrame, StopFrame, CancelFrame implemented in base class
|
|
||||||
#
|
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
||||||
"""Process incoming frames from the pipeline.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame: The frame to process.
|
|
||||||
direction: The direction of frame flow in the pipeline.
|
|
||||||
"""
|
|
||||||
# Backward-compatible dict path: frame.settings contains SessionProperties
|
|
||||||
# fields, not our Settings fields, so we construct SessionProperties
|
|
||||||
# directly. The frame.delta path falls through to super, which calls
|
|
||||||
# _update_settings → our override handles the rest.
|
|
||||||
if isinstance(frame, LLMUpdateSettingsFrame) and frame.delta is None:
|
|
||||||
self._session_properties = events.SessionProperties(**frame.settings)
|
|
||||||
await self._send_session_update()
|
|
||||||
await self.push_frame(frame, direction)
|
|
||||||
return
|
|
||||||
|
|
||||||
await super().process_frame(frame, direction)
|
|
||||||
|
|
||||||
if isinstance(frame, TranscriptionFrame):
|
|
||||||
pass
|
|
||||||
elif isinstance(frame, OpenAILLMContextFrame):
|
|
||||||
context: OpenAIRealtimeLLMContext = OpenAIRealtimeLLMContext.upgrade_to_realtime(
|
|
||||||
frame.context
|
|
||||||
)
|
|
||||||
if not self._context:
|
|
||||||
self._context = context
|
|
||||||
elif frame.context is not self._context:
|
|
||||||
# If the context has changed, reset the conversation
|
|
||||||
self._context = context
|
|
||||||
await self.reset_conversation()
|
|
||||||
# Run the LLM at next opportunity
|
|
||||||
await self._create_response()
|
|
||||||
elif isinstance(frame, LLMContextFrame):
|
|
||||||
raise NotImplementedError(
|
|
||||||
"Universal LLMContext is not yet supported for OpenAI Realtime."
|
|
||||||
)
|
|
||||||
elif isinstance(frame, InputAudioRawFrame):
|
|
||||||
if not self._audio_input_paused:
|
|
||||||
await self._send_user_audio(frame)
|
|
||||||
elif isinstance(frame, InterruptionFrame):
|
|
||||||
await self._handle_interruption()
|
|
||||||
elif isinstance(frame, UserStartedSpeakingFrame):
|
|
||||||
await self._handle_user_started_speaking(frame)
|
|
||||||
elif isinstance(frame, UserStoppedSpeakingFrame):
|
|
||||||
await self._handle_user_stopped_speaking(frame)
|
|
||||||
elif isinstance(frame, BotStoppedSpeakingFrame):
|
|
||||||
await self._handle_bot_stopped_speaking()
|
|
||||||
elif isinstance(frame, LLMMessagesAppendFrame):
|
|
||||||
await self._handle_messages_append(frame)
|
|
||||||
elif isinstance(frame, RealtimeMessagesUpdateFrame):
|
|
||||||
self._context = frame.context
|
|
||||||
elif isinstance(frame, LLMSetToolsFrame):
|
|
||||||
await self._send_session_update()
|
|
||||||
elif isinstance(frame, RealtimeFunctionCallResultFrame):
|
|
||||||
await self._handle_function_call_result(frame.result_frame)
|
|
||||||
|
|
||||||
await self.push_frame(frame, direction)
|
|
||||||
|
|
||||||
async def _handle_messages_append(self, frame):
|
|
||||||
logger.error("!!! NEED TO IMPLEMENT MESSAGES APPEND")
|
|
||||||
|
|
||||||
async def _handle_function_call_result(self, frame):
|
|
||||||
item = events.ConversationItem(
|
|
||||||
type="function_call_output",
|
|
||||||
call_id=frame.tool_call_id,
|
|
||||||
output=json.dumps(frame.result, ensure_ascii=False),
|
|
||||||
)
|
|
||||||
await self.send_client_event(events.ConversationItemCreateEvent(item=item))
|
|
||||||
|
|
||||||
#
|
|
||||||
# websocket communication
|
|
||||||
#
|
|
||||||
|
|
||||||
async def send_client_event(self, event: events.ClientEvent):
|
|
||||||
"""Send a client event to the OpenAI Realtime API.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: The client event to send.
|
|
||||||
"""
|
|
||||||
await self._ws_send(event.model_dump(exclude_none=True))
|
|
||||||
|
|
||||||
async def _connect(self):
|
|
||||||
try:
|
|
||||||
if self._websocket:
|
|
||||||
# Here we assume that if we have a websocket, we are connected. We
|
|
||||||
# handle disconnections in the send/recv code paths.
|
|
||||||
return
|
|
||||||
self._websocket = await websocket_connect(
|
|
||||||
uri=self.base_url,
|
|
||||||
additional_headers={
|
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
|
||||||
"OpenAI-Beta": "realtime=v1",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self._receive_task = self.create_task(self._receive_task_handler())
|
|
||||||
except Exception as e:
|
|
||||||
await self.push_error(error_msg=f"Error connecting: {e}", exception=e)
|
|
||||||
self._websocket = None
|
|
||||||
|
|
||||||
async def _disconnect(self):
|
|
||||||
try:
|
|
||||||
self._disconnecting = True
|
|
||||||
self._api_session_ready = False
|
|
||||||
await self.stop_all_metrics()
|
|
||||||
if self._websocket:
|
|
||||||
await self._websocket.close()
|
|
||||||
self._websocket = None
|
|
||||||
if self._receive_task:
|
|
||||||
await self.cancel_task(self._receive_task, timeout=1.0)
|
|
||||||
self._receive_task = None
|
|
||||||
self._disconnecting = False
|
|
||||||
except Exception as e:
|
|
||||||
await self.push_error(error_msg=f"Error disconnecting: {e}", exception=e)
|
|
||||||
|
|
||||||
async def _ws_send(self, realtime_message):
|
|
||||||
try:
|
|
||||||
if self._websocket:
|
|
||||||
await self._websocket.send(json.dumps(realtime_message))
|
|
||||||
except Exception as e:
|
|
||||||
if self._disconnecting:
|
|
||||||
return
|
|
||||||
# In server-to-server contexts, a WebSocket error should be quite rare. Given how hard
|
|
||||||
# it is to recover from a send-side error with proper state management, and that exponential
|
|
||||||
# backoff for retries can have cost/stability implications for a service cluster, let's just
|
|
||||||
# treat a send-side error as fatal.
|
|
||||||
await self.push_error(error_msg=f"Error sending client event: {e}", exception=e)
|
|
||||||
|
|
||||||
async def _update_settings(self, delta):
|
|
||||||
"""Apply a settings delta."""
|
|
||||||
changed = await super()._update_settings(delta)
|
|
||||||
self._warn_unhandled_updated_settings(changed.keys())
|
|
||||||
return changed
|
|
||||||
|
|
||||||
async def _send_session_update(self):
|
|
||||||
settings = self._session_properties
|
|
||||||
# tools given in the context override the tools in the session properties
|
|
||||||
if self._context and self._context.tools:
|
|
||||||
settings.tools = self._context.tools
|
|
||||||
# instructions in the context come from an initial "system" message in the
|
|
||||||
# messages list, and override instructions in the session properties
|
|
||||||
if self._context and self._context._session_instructions:
|
|
||||||
settings.instructions = self._context._session_instructions
|
|
||||||
await self.send_client_event(events.SessionUpdateEvent(session=settings))
|
|
||||||
|
|
||||||
#
|
|
||||||
# inbound server event handling
|
|
||||||
# https://platform.openai.com/docs/api-reference/realtime-server-events
|
|
||||||
#
|
|
||||||
|
|
||||||
async def _receive_task_handler(self):
|
|
||||||
async for message in self._websocket:
|
|
||||||
evt = events.parse_server_event(message)
|
|
||||||
if evt.type == "session.created":
|
|
||||||
await self._handle_evt_session_created(evt)
|
|
||||||
elif evt.type == "session.updated":
|
|
||||||
await self._handle_evt_session_updated(evt)
|
|
||||||
elif evt.type == "response.audio.delta":
|
|
||||||
await self._handle_evt_audio_delta(evt)
|
|
||||||
elif evt.type == "response.audio.done":
|
|
||||||
await self._handle_evt_audio_done(evt)
|
|
||||||
elif evt.type == "conversation.item.created":
|
|
||||||
await self._handle_evt_conversation_item_created(evt)
|
|
||||||
elif evt.type == "conversation.item.input_audio_transcription.delta":
|
|
||||||
await self._handle_evt_input_audio_transcription_delta(evt)
|
|
||||||
elif evt.type == "conversation.item.input_audio_transcription.completed":
|
|
||||||
await self.handle_evt_input_audio_transcription_completed(evt)
|
|
||||||
elif evt.type == "conversation.item.retrieved":
|
|
||||||
await self._handle_conversation_item_retrieved(evt)
|
|
||||||
elif evt.type == "response.done":
|
|
||||||
await self._handle_evt_response_done(evt)
|
|
||||||
elif evt.type == "input_audio_buffer.speech_started":
|
|
||||||
await self._handle_evt_speech_started(evt)
|
|
||||||
elif evt.type == "input_audio_buffer.speech_stopped":
|
|
||||||
await self._handle_evt_speech_stopped(evt)
|
|
||||||
elif evt.type == "response.text.delta":
|
|
||||||
await self._handle_evt_text_delta(evt)
|
|
||||||
elif evt.type == "response.audio_transcript.delta":
|
|
||||||
await self._handle_evt_audio_transcript_delta(evt)
|
|
||||||
elif evt.type == "error":
|
|
||||||
if not await self._maybe_handle_evt_retrieve_conversation_item_error(evt):
|
|
||||||
if evt.error.code in (
|
|
||||||
"response_cancel_not_active",
|
|
||||||
"conversation_already_has_active_response",
|
|
||||||
):
|
|
||||||
logger.debug(f"{self} {evt.error.message}")
|
|
||||||
else:
|
|
||||||
await self._handle_evt_error(evt)
|
|
||||||
# errors are fatal, so exit the receive loop
|
|
||||||
return
|
|
||||||
|
|
||||||
@traced_openai_realtime(operation="llm_setup")
|
|
||||||
async def _handle_evt_session_created(self, evt):
|
|
||||||
# session.created is received right after connecting. Send a message
|
|
||||||
# to configure the session properties.
|
|
||||||
await self._send_session_update()
|
|
||||||
|
|
||||||
async def _handle_evt_session_updated(self, evt):
|
|
||||||
# If this is our first context frame, run the LLM
|
|
||||||
self._api_session_ready = True
|
|
||||||
# Now that we've configured the session, we can run the LLM if we need to.
|
|
||||||
if self._run_llm_when_api_session_ready:
|
|
||||||
self._run_llm_when_api_session_ready = False
|
|
||||||
await self._create_response()
|
|
||||||
|
|
||||||
async def _handle_evt_audio_delta(self, evt):
|
|
||||||
# note: ttfb is faster by 1/2 RTT than ttfb as measured for other services, since we're getting
|
|
||||||
# this event from the server
|
|
||||||
await self.stop_ttfb_metrics()
|
|
||||||
|
|
||||||
if self._current_audio_response and self._current_audio_response.item_id != evt.item_id:
|
|
||||||
logger.warning(
|
|
||||||
f"Received a new audio delta for an already completed audio response before receiving the BotStoppedSpeakingFrame."
|
|
||||||
)
|
|
||||||
logger.debug("Forcing previous audio response to None")
|
|
||||||
self._current_audio_response = None
|
|
||||||
|
|
||||||
if not self._current_audio_response:
|
|
||||||
self._current_audio_response = CurrentAudioResponse(
|
|
||||||
item_id=evt.item_id,
|
|
||||||
content_index=evt.content_index,
|
|
||||||
start_time_ms=int(time.time() * 1000),
|
|
||||||
)
|
|
||||||
await self.push_frame(TTSStartedFrame())
|
|
||||||
audio = base64.b64decode(evt.delta)
|
|
||||||
self._current_audio_response.total_size += len(audio)
|
|
||||||
frame = TTSAudioRawFrame(
|
|
||||||
audio=audio,
|
|
||||||
sample_rate=24000,
|
|
||||||
num_channels=1,
|
|
||||||
)
|
|
||||||
await self.push_frame(frame)
|
|
||||||
|
|
||||||
async def _handle_evt_audio_done(self, evt):
|
|
||||||
if self._current_audio_response:
|
|
||||||
await self.push_frame(TTSStoppedFrame())
|
|
||||||
# Don't clear the self._current_audio_response here. We need to wait until we
|
|
||||||
# receive a BotStoppedSpeakingFrame from the output transport.
|
|
||||||
|
|
||||||
async def _handle_evt_conversation_item_created(self, evt):
|
|
||||||
await self._call_event_handler("on_conversation_item_created", evt.item.id, evt.item)
|
|
||||||
|
|
||||||
# This will get sent from the server every time a new "message" is added
|
|
||||||
# to the server's conversation state, whether we create it via the API
|
|
||||||
# or the server creates it from LLM output.
|
|
||||||
if self._messages_added_manually.get(evt.item.id):
|
|
||||||
del self._messages_added_manually[evt.item.id]
|
|
||||||
return
|
|
||||||
|
|
||||||
if evt.item.role == "user":
|
|
||||||
# We need to wait for completion of both user message and response message. Then we'll
|
|
||||||
# add both to the context. User message is complete when we have a "transcript" field
|
|
||||||
# that is not None. Response message is complete when we get a "response.done" event.
|
|
||||||
self._user_and_response_message_tuple = (evt.item, {"done": False, "output": []})
|
|
||||||
elif evt.item.role == "assistant":
|
|
||||||
self._current_assistant_response = evt.item
|
|
||||||
await self.push_frame(LLMFullResponseStartFrame())
|
|
||||||
|
|
||||||
async def _handle_evt_input_audio_transcription_delta(self, evt):
|
|
||||||
if self._send_transcription_frames:
|
|
||||||
await self.push_frame(
|
|
||||||
# no way to get a language code?
|
|
||||||
InterimTranscriptionFrame(evt.delta, "", time_now_iso8601(), result=evt)
|
|
||||||
)
|
|
||||||
|
|
||||||
@traced_stt
|
|
||||||
async def _handle_user_transcription(
|
|
||||||
self, transcript: str, is_final: bool, language: Optional[Language] = None
|
|
||||||
):
|
|
||||||
"""Handle a transcription result with tracing."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def handle_evt_input_audio_transcription_completed(self, evt):
|
|
||||||
"""Handle completion of input audio transcription.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
evt: The transcription completed event.
|
|
||||||
"""
|
|
||||||
await self._call_event_handler("on_conversation_item_updated", evt.item_id, None)
|
|
||||||
|
|
||||||
if self._send_transcription_frames:
|
|
||||||
await self.push_frame(
|
|
||||||
# no way to get a language code?
|
|
||||||
TranscriptionFrame(evt.transcript, "", time_now_iso8601(), result=evt)
|
|
||||||
)
|
|
||||||
await self._handle_user_transcription(evt.transcript, True, Language.EN)
|
|
||||||
pair = self._user_and_response_message_tuple
|
|
||||||
if pair:
|
|
||||||
user, assistant = pair
|
|
||||||
user.content[0].transcript = evt.transcript
|
|
||||||
if assistant["done"]:
|
|
||||||
self._user_and_response_message_tuple = None
|
|
||||||
self._context.add_user_content_item_as_message(user)
|
|
||||||
await self._handle_assistant_output(assistant["output"])
|
|
||||||
else:
|
|
||||||
# User message without preceding conversation.item.created. Bug?
|
|
||||||
logger.warning(f"Transcript for unknown user message: {evt}")
|
|
||||||
|
|
||||||
async def _handle_conversation_item_retrieved(self, evt: events.ConversationItemRetrieved):
|
|
||||||
futures = self._retrieve_conversation_item_futures.pop(evt.item.id, None)
|
|
||||||
if futures:
|
|
||||||
for future in futures:
|
|
||||||
future.set_result(evt.item)
|
|
||||||
|
|
||||||
@traced_openai_realtime(operation="llm_response")
|
|
||||||
async def _handle_evt_response_done(self, evt):
|
|
||||||
# todo: figure out whether there's anything we need to do for "cancelled" events
|
|
||||||
# usage metrics
|
|
||||||
tokens = LLMTokenUsage(
|
|
||||||
prompt_tokens=evt.response.usage.input_tokens,
|
|
||||||
completion_tokens=evt.response.usage.output_tokens,
|
|
||||||
total_tokens=evt.response.usage.total_tokens,
|
|
||||||
)
|
|
||||||
await self.start_llm_usage_metrics(tokens)
|
|
||||||
await self.stop_processing_metrics()
|
|
||||||
await self.push_frame(LLMFullResponseEndFrame())
|
|
||||||
self._current_assistant_response = None
|
|
||||||
# error handling
|
|
||||||
if evt.response.status == "failed":
|
|
||||||
await self.push_error(ErrorFrame(error=evt.response.status_details["error"]["message"]))
|
|
||||||
return
|
|
||||||
# response content
|
|
||||||
for item in evt.response.output:
|
|
||||||
await self._call_event_handler("on_conversation_item_updated", item.id, item)
|
|
||||||
pair = self._user_and_response_message_tuple
|
|
||||||
if pair:
|
|
||||||
user, assistant = pair
|
|
||||||
assistant["done"] = True
|
|
||||||
assistant["output"] = evt.response.output
|
|
||||||
if user.content[0].transcript is not None:
|
|
||||||
self._user_and_response_message_tuple = None
|
|
||||||
self._context.add_user_content_item_as_message(user)
|
|
||||||
await self._handle_assistant_output(assistant["output"])
|
|
||||||
else:
|
|
||||||
# Response message without preceding user message. Add it to the context.
|
|
||||||
await self._handle_assistant_output(evt.response.output)
|
|
||||||
|
|
||||||
async def _handle_evt_text_delta(self, evt):
|
|
||||||
if evt.delta:
|
|
||||||
await self.push_frame(LLMTextFrame(evt.delta))
|
|
||||||
|
|
||||||
async def _handle_evt_audio_transcript_delta(self, evt):
|
|
||||||
if evt.delta:
|
|
||||||
await self.push_frame(LLMTextFrame(evt.delta))
|
|
||||||
await self.push_frame(TTSTextFrame(evt.delta, aggregated_by=AggregationType.SENTENCE))
|
|
||||||
|
|
||||||
async def _handle_evt_speech_started(self, evt):
|
|
||||||
await self._truncate_current_audio_response()
|
|
||||||
await self.broadcast_frame(UserStartedSpeakingFrame)
|
|
||||||
await self.broadcast_interruption()
|
|
||||||
|
|
||||||
async def _handle_evt_speech_stopped(self, evt):
|
|
||||||
await self.start_ttfb_metrics()
|
|
||||||
await self.start_processing_metrics()
|
|
||||||
await self.broadcast_frame(UserStoppedSpeakingFrame)
|
|
||||||
|
|
||||||
async def _maybe_handle_evt_retrieve_conversation_item_error(self, evt: events.ErrorEvent):
|
|
||||||
"""Maybe handle an error event related to retrieving a conversation item.
|
|
||||||
|
|
||||||
If the given error event is an error retrieving a conversation item:
|
|
||||||
|
|
||||||
- set an exception on the future that retrieve_conversation_item() is waiting on
|
|
||||||
- return true
|
|
||||||
Otherwise:
|
|
||||||
- return false
|
|
||||||
"""
|
|
||||||
if evt.error.code == "item_retrieve_invalid_item_id":
|
|
||||||
item_id = evt.error.event_id.split("_", 1)[1] # event_id is of the form "rci_{item_id}"
|
|
||||||
futures = self._retrieve_conversation_item_futures.pop(item_id, None)
|
|
||||||
if futures:
|
|
||||||
for future in futures:
|
|
||||||
future.set_exception(Exception(evt.error.message))
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def _handle_evt_error(self, evt):
|
|
||||||
# Errors are fatal to this connection. Send an ErrorFrame.
|
|
||||||
await self.push_error(error_msg=f"Error: {evt}")
|
|
||||||
|
|
||||||
async def _handle_assistant_output(self, output):
|
|
||||||
# We haven't seen intermixed audio and function_call items in the same response. But let's
|
|
||||||
# try to write logic that handles that, if it does happen.
|
|
||||||
# Also, the assistant output is pushed as LLMTextFrame and TTSTextFrame to be handled by
|
|
||||||
# the assistant context aggregator.
|
|
||||||
function_calls = [item for item in output if item.type == "function_call"]
|
|
||||||
await self._handle_function_call_items(function_calls)
|
|
||||||
|
|
||||||
async def _handle_function_call_items(self, items):
|
|
||||||
function_calls = []
|
|
||||||
for item in items:
|
|
||||||
args = json.loads(item.arguments)
|
|
||||||
function_calls.append(
|
|
||||||
FunctionCallFromLLM(
|
|
||||||
context=self._context,
|
|
||||||
tool_call_id=item.call_id,
|
|
||||||
function_name=item.name,
|
|
||||||
arguments=args,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await self.run_function_calls(function_calls)
|
|
||||||
|
|
||||||
#
|
|
||||||
# state and client events for the current conversation
|
|
||||||
# https://platform.openai.com/docs/api-reference/realtime-client-events
|
|
||||||
#
|
|
||||||
|
|
||||||
async def reset_conversation(self):
|
|
||||||
"""Reset the conversation by disconnecting and reconnecting.
|
|
||||||
|
|
||||||
This is the safest way to start a new conversation. Note that this will
|
|
||||||
fail if called from the receive task.
|
|
||||||
"""
|
|
||||||
logger.debug("Resetting conversation")
|
|
||||||
await self._disconnect()
|
|
||||||
if self._context:
|
|
||||||
self._context.llm_needs_settings_update = True
|
|
||||||
self._context.llm_needs_initial_messages = True
|
|
||||||
await self._connect()
|
|
||||||
|
|
||||||
@traced_openai_realtime(operation="llm_request")
|
|
||||||
async def _create_response(self):
|
|
||||||
if not self._api_session_ready:
|
|
||||||
self._run_llm_when_api_session_ready = True
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._context.llm_needs_initial_messages:
|
|
||||||
messages = self._context.get_messages_for_initializing_history()
|
|
||||||
for item in messages:
|
|
||||||
evt = events.ConversationItemCreateEvent(item=item)
|
|
||||||
self._messages_added_manually[evt.item.id] = True
|
|
||||||
await self.send_client_event(evt)
|
|
||||||
self._context.llm_needs_initial_messages = False
|
|
||||||
|
|
||||||
if self._context.llm_needs_settings_update:
|
|
||||||
await self._send_session_update()
|
|
||||||
self._context.llm_needs_settings_update = False
|
|
||||||
|
|
||||||
logger.debug(f"Creating response: {self._context.get_messages_for_logging()}")
|
|
||||||
|
|
||||||
await self.push_frame(LLMFullResponseStartFrame())
|
|
||||||
await self.start_processing_metrics()
|
|
||||||
await self.start_ttfb_metrics()
|
|
||||||
await self.send_client_event(
|
|
||||||
events.ResponseCreateEvent(
|
|
||||||
response=events.ResponseProperties(modalities=self._get_enabled_modalities())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _send_user_audio(self, frame):
|
|
||||||
payload = base64.b64encode(frame.audio).decode("utf-8")
|
|
||||||
await self.send_client_event(events.InputAudioBufferAppendEvent(audio=payload))
|
|
||||||
|
|
||||||
def create_context_aggregator(
|
|
||||||
self,
|
|
||||||
context: OpenAILLMContext,
|
|
||||||
*,
|
|
||||||
user_params: LLMUserAggregatorParams = LLMUserAggregatorParams(),
|
|
||||||
assistant_params: LLMAssistantAggregatorParams = LLMAssistantAggregatorParams(),
|
|
||||||
) -> OpenAIContextAggregatorPair:
|
|
||||||
"""Create an instance of OpenAIContextAggregatorPair from an OpenAILLMContext.
|
|
||||||
|
|
||||||
Constructor keyword arguments for both the user and assistant aggregators can be provided.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context: The LLM context.
|
|
||||||
user_params: User aggregator parameters.
|
|
||||||
assistant_params: Assistant aggregator parameters.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
OpenAIContextAggregatorPair: A pair of context aggregators, one for
|
|
||||||
the user and one for the assistant, encapsulated in an
|
|
||||||
OpenAIContextAggregatorPair.
|
|
||||||
"""
|
|
||||||
context.set_llm_adapter(self.get_llm_adapter())
|
|
||||||
|
|
||||||
OpenAIRealtimeLLMContext.upgrade_to_realtime(context)
|
|
||||||
user = OpenAIRealtimeUserContextAggregator(context, params=user_params)
|
|
||||||
|
|
||||||
assistant_params.expect_stripped_words = False
|
|
||||||
assistant = OpenAIRealtimeAssistantContextAggregator(context, params=assistant_params)
|
|
||||||
return OpenAIContextAggregatorPair(_user=user, _assistant=assistant)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pipecat.services import DeprecatedModuleProxy
|
|
||||||
|
|
||||||
from .stt import *
|
|
||||||
from .tts import *
|
|
||||||
|
|
||||||
sys.modules[__name__] = DeprecatedModuleProxy(globals(), "riva", "riva.[stt,tts]")
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""NVIDIA Riva Speech-to-Text service implementations for real-time and batch transcription.
|
|
||||||
|
|
||||||
.. deprecated:: 0.0.96
|
|
||||||
This module is deprecated. Please NvidiaSTTService from
|
|
||||||
pipecat.services.nvidia.stt instead.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pipecat.services.nvidia.stt import (
|
|
||||||
NvidiaSegmentedSTTService,
|
|
||||||
NvidiaSTTService,
|
|
||||||
language_to_nvidia_riva_language,
|
|
||||||
)
|
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
warnings.warn(
|
|
||||||
"RivaSTTService and ParakeetSTTService "
|
|
||||||
"from pipecat.services.riva.stt is deprecated. "
|
|
||||||
"Please use NvidiaSTTService from pipecat.services.nvidia.stt instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
RivaSTTService = NvidiaSTTService
|
|
||||||
language_to_riva_language = language_to_nvidia_riva_language
|
|
||||||
RivaSegmentedSTTService = NvidiaSegmentedSTTService
|
|
||||||
ParakeetSTTService = NvidiaSTTService
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""NVIDIA Riva text-to-speech service implementation.
|
|
||||||
|
|
||||||
This module provides integration with NVIDIA Riva's TTS services through
|
|
||||||
gRPC API for high-quality speech synthesis.
|
|
||||||
|
|
||||||
.. deprecated:: 0.0.96
|
|
||||||
This module is deprecated. Please NvidiaTTSService from
|
|
||||||
pipecat.services.nvidia.tts instead.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pipecat.services.nvidia.tts import NVIDIA_TTS_TIMEOUT_SECS, NvidiaTTSService
|
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
warnings.warn(
|
|
||||||
"FastPitchTTSService and RivaTTSService "
|
|
||||||
"from pipecat.services.nim.llm are deprecated. "
|
|
||||||
"Please use NvidiaLLMService from pipecat.services.nvidia.tts instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
RivaTTSService = NvidiaTTSService
|
|
||||||
FastPitchTTSService = NvidiaTTSService
|
|
||||||
RIVA_TTS_TIMEOUT_SECS = NVIDIA_TTS_TIMEOUT_SECS
|
|
||||||
@@ -47,7 +47,6 @@ def _get_provider_name_from_service_name(service_name: str) -> str:
|
|||||||
"AzureLLMService": "az.ai.openai",
|
"AzureLLMService": "az.ai.openai",
|
||||||
# Google
|
# Google
|
||||||
"GoogleLLMService": "gcp.gemini",
|
"GoogleLLMService": "gcp.gemini",
|
||||||
"GoogleLLMOpenAIBetaService": "gcp.gemini",
|
|
||||||
"GoogleVertexLLMService": "gcp.vertex_ai",
|
"GoogleVertexLLMService": "gcp.vertex_ai",
|
||||||
# Others
|
# Others
|
||||||
"GrokLLMService": "xai",
|
"GrokLLMService": "xai",
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024-2026, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Unit tests for Google LLM OpenAI Beta service."""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import warnings
|
|
||||||
from unittest.mock import AsyncMock, patch
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
|
||||||
|
|
||||||
try:
|
|
||||||
from pipecat.services.google.openai.llm import GoogleLLMOpenAIBetaService
|
|
||||||
|
|
||||||
google_available = True
|
|
||||||
except Exception:
|
|
||||||
google_available = False
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
@pytest.mark.skipif(not google_available, reason="Google dependencies not installed")
|
|
||||||
async def test_google_llm_openai_stream_closed_on_cancellation():
|
|
||||||
"""Test that the stream is closed when CancelledError occurs during iteration.
|
|
||||||
|
|
||||||
This prevents socket leaks when the pipeline is interrupted (e.g., user interruption).
|
|
||||||
See issue #3639.
|
|
||||||
"""
|
|
||||||
with patch.object(GoogleLLMOpenAIBetaService, "create_client"):
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("ignore", DeprecationWarning)
|
|
||||||
service = GoogleLLMOpenAIBetaService(api_key="test-key", model="test-model")
|
|
||||||
service._client = AsyncMock()
|
|
||||||
|
|
||||||
stream_closed = False
|
|
||||||
|
|
||||||
class MockAsyncStream:
|
|
||||||
"""Mock AsyncStream that tracks close() calls and raises CancelledError."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.iteration_count = 0
|
|
||||||
|
|
||||||
async def __aenter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
nonlocal stream_closed
|
|
||||||
stream_closed = True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __aiter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def __anext__(self):
|
|
||||||
self.iteration_count += 1
|
|
||||||
if self.iteration_count > 1:
|
|
||||||
raise asyncio.CancelledError()
|
|
||||||
mock_chunk = AsyncMock()
|
|
||||||
mock_chunk.usage = None
|
|
||||||
mock_chunk.choices = []
|
|
||||||
return mock_chunk
|
|
||||||
|
|
||||||
mock_stream = MockAsyncStream()
|
|
||||||
|
|
||||||
service._stream_chat_completions_specific_context = AsyncMock(return_value=mock_stream)
|
|
||||||
service.start_ttfb_metrics = AsyncMock()
|
|
||||||
service.stop_ttfb_metrics = AsyncMock()
|
|
||||||
service.start_llm_usage_metrics = AsyncMock()
|
|
||||||
|
|
||||||
context = OpenAILLMContext(
|
|
||||||
messages=[{"role": "user", "content": "Hello"}],
|
|
||||||
)
|
|
||||||
|
|
||||||
with pytest.raises(asyncio.CancelledError):
|
|
||||||
await service._process_context(context)
|
|
||||||
|
|
||||||
assert stream_closed, "Stream should be closed even when CancelledError occurs"
|
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from pipecat.services.deepgram.sagemaker.stt import DeepgramSageMakerSTTSettings
|
||||||
from pipecat.services.deepgram.stt import DeepgramSTTService, DeepgramSTTSettings
|
from pipecat.services.deepgram.stt import DeepgramSTTService, DeepgramSTTSettings
|
||||||
from pipecat.services.deepgram.stt_sagemaker import DeepgramSageMakerSTTSettings
|
|
||||||
from pipecat.services.openai.realtime import events
|
from pipecat.services.openai.realtime import events
|
||||||
from pipecat.services.openai.realtime.llm import OpenAIRealtimeLLMSettings
|
from pipecat.services.openai.realtime.llm import OpenAIRealtimeLLMSettings
|
||||||
from pipecat.services.settings import (
|
from pipecat.services.settings import (
|
||||||
|
|||||||
Reference in New Issue
Block a user