Update OpenAIRealtimeBetaLLMService docstrings

This commit is contained in:
Mark Backman
2025-06-26 12:06:47 -04:00
parent d123cd4b2b
commit d8ce108ccd
4 changed files with 688 additions and 14 deletions

View File

@@ -4,6 +4,8 @@
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
"""OpenAI Realtime LLM context and aggregator implementations."""
import copy import copy
import json import json
@@ -30,6 +32,18 @@ from .frames import RealtimeFunctionCallResultFrame, RealtimeMessagesUpdateFrame
class OpenAIRealtimeLLMContext(OpenAILLMContext): 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.
Args:
messages: Initial conversation messages. Defaults to None.
tools: Available function tools. Defaults to None.
**kwargs: Additional arguments passed to parent OpenAILLMContext.
"""
def __init__(self, messages=None, tools=None, **kwargs): def __init__(self, messages=None, tools=None, **kwargs):
super().__init__(messages=messages, tools=tools, **kwargs) super().__init__(messages=messages, tools=tools, **kwargs)
self.__setup_local() self.__setup_local()
@@ -43,6 +57,14 @@ class OpenAIRealtimeLLMContext(OpenAILLMContext):
@staticmethod @staticmethod
def upgrade_to_realtime(obj: OpenAILLMContext) -> "OpenAIRealtimeLLMContext": 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): if isinstance(obj, OpenAILLMContext) and not isinstance(obj, OpenAIRealtimeLLMContext):
obj.__class__ = OpenAIRealtimeLLMContext obj.__class__ = OpenAIRealtimeLLMContext
obj.__setup_local() obj.__setup_local()
@@ -52,6 +74,14 @@ class OpenAIRealtimeLLMContext(OpenAILLMContext):
# - finish implementing all frames # - finish implementing all frames
def from_standard_message(self, message): 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": if message.get("role") == "user":
content = message.get("content") content = message.get("content")
if isinstance(message.get("content"), list): if isinstance(message.get("content"), list):
@@ -79,6 +109,14 @@ class OpenAIRealtimeLLMContext(OpenAILLMContext):
logger.error(f"Unhandled message type in from_standard_message: {message}") logger.error(f"Unhandled message type in from_standard_message: {message}")
def get_messages_for_initializing_history(self): 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 # 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 # 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" # our general strategy until this is fixed is just to put everything into a first "user"
@@ -133,6 +171,11 @@ class OpenAIRealtimeLLMContext(OpenAILLMContext):
] ]
def add_user_content_item_as_message(self, item): 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 = { message = {
"role": "user", "role": "user",
"content": [{"type": "text", "text": item.content[0].transcript}], "content": [{"type": "text", "text": item.content[0].transcript}],
@@ -141,9 +184,25 @@ class OpenAIRealtimeLLMContext(OpenAILLMContext):
class OpenAIRealtimeUserContextAggregator(OpenAIUserContextAggregator): 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( async def process_frame(
self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM 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) await super().process_frame(frame, direction)
# Parent does not push LLMMessagesUpdateFrame. This ensures that in a typical pipeline, # 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 # messages are only processed by the user context aggregator, which is generally what we want. But
@@ -157,6 +216,11 @@ class OpenAIRealtimeUserContextAggregator(OpenAIUserContextAggregator):
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
async def push_aggregation(self): 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. # 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 # todo: think about whether/how to fix this to allow for text input from
# upstream (transport/transcription, or other sources) # upstream (transport/transcription, or other sources)
@@ -164,6 +228,16 @@ class OpenAIRealtimeUserContextAggregator(OpenAIUserContextAggregator):
class OpenAIRealtimeAssistantContextAggregator(OpenAIAssistantContextAggregator): 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, # The LLMAssistantContextAggregator uses TextFrames to aggregate the LLM output,
# but the OpenAIRealtimeLLMService pushes LLMTextFrames and TTSTextFrames. We # but the OpenAIRealtimeLLMService pushes LLMTextFrames and TTSTextFrames. We
# need to override this proces_frame for LLMTextFrame, so that only the TTSTextFrames # need to override this proces_frame for LLMTextFrame, so that only the TTSTextFrames
@@ -171,10 +245,21 @@ class OpenAIRealtimeAssistantContextAggregator(OpenAIAssistantContextAggregator)
# OpenAIRealtimeLLMService also pushes TranscriptionFrames and InterimTranscriptionFrames, # OpenAIRealtimeLLMService also pushes TranscriptionFrames and InterimTranscriptionFrames,
# so we need to ignore pushing those as well, as they're also TextFrames. # so we need to ignore pushing those as well, as they're also TextFrames.
async def process_frame(self, frame: Frame, direction: FrameDirection): 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)): if not isinstance(frame, (LLMTextFrame, TranscriptionFrame, InterimTranscriptionFrame)):
await super().process_frame(frame, direction) await super().process_frame(frame, direction)
async def handle_function_call_result(self, frame: FunctionCallResultFrame): 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) await super().handle_function_call_result(frame)
# The standard function callback code path pushes the FunctionCallResultFrame from the llm itself, # The standard function callback code path pushes the FunctionCallResultFrame from the llm itself,

View File

@@ -3,7 +3,8 @@
# #
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
#
"""Event models and data structures for OpenAI Realtime API communication."""
import json import json
import uuid import uuid
@@ -19,7 +20,7 @@ from pydantic import BaseModel, ConfigDict, Field
class InputAudioTranscription(BaseModel): class InputAudioTranscription(BaseModel):
"""Configuration for audio transcription settings. """Configuration for audio transcription settings.
Attributes: Parameters:
model: Transcription model to use (e.g., "gpt-4o-transcribe", "whisper-1"). model: Transcription model to use (e.g., "gpt-4o-transcribe", "whisper-1").
language: Optional language code for transcription. language: Optional language code for transcription.
prompt: Optional transcription hint text. prompt: Optional transcription hint text.
@@ -39,6 +40,15 @@ class InputAudioTranscription(BaseModel):
class TurnDetection(BaseModel): 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" type: Optional[Literal["server_vad"]] = "server_vad"
threshold: Optional[float] = 0.5 threshold: Optional[float] = 0.5
prefix_padding_ms: Optional[int] = 300 prefix_padding_ms: Optional[int] = 300
@@ -46,6 +56,15 @@ class TurnDetection(BaseModel):
class SemanticTurnDetection(BaseModel): 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" type: Optional[Literal["semantic_vad"]] = "semantic_vad"
eagerness: Optional[Literal["low", "medium", "high", "auto"]] = None eagerness: Optional[Literal["low", "medium", "high", "auto"]] = None
create_response: Optional[bool] = None create_response: Optional[bool] = None
@@ -53,10 +72,33 @@ class SemanticTurnDetection(BaseModel):
class InputAudioNoiseReduction(BaseModel): class InputAudioNoiseReduction(BaseModel):
"""Input audio noise reduction configuration.
Parameters:
type: Noise reduction type for different microphone scenarios.
"""
type: Optional[Literal["near_field", "far_field"]] type: Optional[Literal["near_field", "far_field"]]
class SessionProperties(BaseModel): 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 modalities: Optional[List[Literal["text", "audio"]]] = None
instructions: Optional[str] = None instructions: Optional[str] = None
voice: Optional[str] = None voice: Optional[str] = None
@@ -80,6 +122,15 @@ class SessionProperties(BaseModel):
class ItemContent(BaseModel): 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"] type: Literal["text", "audio", "input_text", "input_audio"]
text: Optional[str] = None text: Optional[str] = None
audio: Optional[str] = None # base64-encoded audio audio: Optional[str] = None # base64-encoded audio
@@ -87,6 +138,21 @@ class ItemContent(BaseModel):
class ConversationItem(BaseModel): 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)) id: str = Field(default_factory=lambda: str(uuid.uuid4().hex))
object: Optional[Literal["realtime.item"]] = None object: Optional[Literal["realtime.item"]] = None
type: Literal["message", "function_call", "function_call_output"] type: Literal["message", "function_call", "function_call_output"]
@@ -102,11 +168,31 @@ class ConversationItem(BaseModel):
class RealtimeConversation(BaseModel): class RealtimeConversation(BaseModel):
"""A realtime conversation session.
Parameters:
id: Unique identifier for the conversation.
object: Object type identifier, always "realtime.conversation".
"""
id: str id: str
object: Literal["realtime.conversation"] object: Literal["realtime.conversation"]
class ResponseProperties(BaseModel): 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"] modalities: Optional[List[Literal["text", "audio"]]] = ["audio", "text"]
instructions: Optional[str] = None instructions: Optional[str] = None
voice: Optional[str] = None voice: Optional[str] = None
@@ -121,6 +207,16 @@ class ResponseProperties(BaseModel):
# error class # error class
# #
class RealtimeError(BaseModel): 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 type: str
code: Optional[str] = "" code: Optional[str] = ""
message: str message: str
@@ -134,14 +230,38 @@ class RealtimeError(BaseModel):
class ClientEvent(BaseModel): 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())) event_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
class SessionUpdateEvent(ClientEvent): 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" type: Literal["session.update"] = "session.update"
session: SessionProperties session: SessionProperties
def model_dump(self, *args, **kwargs) -> Dict[str, Any]: 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) dump = super().model_dump(*args, **kwargs)
# Handle turn_detection so that False is serialized as null # Handle turn_detection so that False is serialized as null
@@ -153,25 +273,61 @@ class SessionUpdateEvent(ClientEvent):
class InputAudioBufferAppendEvent(ClientEvent): 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" type: Literal["input_audio_buffer.append"] = "input_audio_buffer.append"
audio: str # base64-encoded audio audio: str # base64-encoded audio
class InputAudioBufferCommitEvent(ClientEvent): 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" type: Literal["input_audio_buffer.commit"] = "input_audio_buffer.commit"
class InputAudioBufferClearEvent(ClientEvent): 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" type: Literal["input_audio_buffer.clear"] = "input_audio_buffer.clear"
class ConversationItemCreateEvent(ClientEvent): 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" type: Literal["conversation.item.create"] = "conversation.item.create"
previous_item_id: Optional[str] = None previous_item_id: Optional[str] = None
item: ConversationItem item: ConversationItem
class ConversationItemTruncateEvent(ClientEvent): 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" type: Literal["conversation.item.truncate"] = "conversation.item.truncate"
item_id: str item_id: str
content_index: int content_index: int
@@ -179,21 +335,48 @@ class ConversationItemTruncateEvent(ClientEvent):
class ConversationItemDeleteEvent(ClientEvent): 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" type: Literal["conversation.item.delete"] = "conversation.item.delete"
item_id: str item_id: str
class ConversationItemRetrieveEvent(ClientEvent): 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" type: Literal["conversation.item.retrieve"] = "conversation.item.retrieve"
item_id: str item_id: str
class ResponseCreateEvent(ClientEvent): 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" type: Literal["response.create"] = "response.create"
response: Optional[ResponseProperties] = None response: Optional[ResponseProperties] = None
class ResponseCancelEvent(ClientEvent): class ResponseCancelEvent(ClientEvent):
"""Event to cancel the current assistant response.
Parameters:
type: Event type, always "response.cancel".
"""
type: Literal["response.cancel"] = "response.cancel" type: Literal["response.cancel"] = "response.cancel"
@@ -203,6 +386,13 @@ class ResponseCancelEvent(ClientEvent):
class ServerEvent(BaseModel): 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) model_config = ConfigDict(arbitrary_types_allowed=True)
event_id: str event_id: str
@@ -210,27 +400,65 @@ class ServerEvent(BaseModel):
class SessionCreatedEvent(ServerEvent): 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"] type: Literal["session.created"]
session: SessionProperties session: SessionProperties
class SessionUpdatedEvent(ServerEvent): 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"] type: Literal["session.updated"]
session: SessionProperties session: SessionProperties
class ConversationCreated(ServerEvent): 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"] type: Literal["conversation.created"]
conversation: RealtimeConversation conversation: RealtimeConversation
class ConversationItemCreated(ServerEvent): 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"] type: Literal["conversation.item.created"]
previous_item_id: Optional[str] = None previous_item_id: Optional[str] = None
item: ConversationItem item: ConversationItem
class ConversationItemInputAudioTranscriptionDelta(ServerEvent): 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"] type: Literal["conversation.item.input_audio_transcription.delta"]
item_id: str item_id: str
content_index: int content_index: int
@@ -238,6 +466,15 @@ class ConversationItemInputAudioTranscriptionDelta(ServerEvent):
class ConversationItemInputAudioTranscriptionCompleted(ServerEvent): 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"] type: Literal["conversation.item.input_audio_transcription.completed"]
item_id: str item_id: str
content_index: int content_index: int
@@ -245,6 +482,15 @@ class ConversationItemInputAudioTranscriptionCompleted(ServerEvent):
class ConversationItemInputAudioTranscriptionFailed(ServerEvent): 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"] type: Literal["conversation.item.input_audio_transcription.failed"]
item_id: str item_id: str
content_index: int content_index: int
@@ -252,6 +498,15 @@ class ConversationItemInputAudioTranscriptionFailed(ServerEvent):
class ConversationItemTruncated(ServerEvent): 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"] type: Literal["conversation.item.truncated"]
item_id: str item_id: str
content_index: int content_index: int
@@ -259,26 +514,63 @@ class ConversationItemTruncated(ServerEvent):
class ConversationItemDeleted(ServerEvent): 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"] type: Literal["conversation.item.deleted"]
item_id: str item_id: str
class ConversationItemRetrieved(ServerEvent): 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"] type: Literal["conversation.item.retrieved"]
item: ConversationItem item: ConversationItem
class ResponseCreated(ServerEvent): 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"] type: Literal["response.created"]
response: "Response" response: "Response"
class ResponseDone(ServerEvent): 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"] type: Literal["response.done"]
response: "Response" response: "Response"
class ResponseOutputItemAdded(ServerEvent): 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"] type: Literal["response.output_item.added"]
response_id: str response_id: str
output_index: int output_index: int
@@ -286,6 +578,15 @@ class ResponseOutputItemAdded(ServerEvent):
class ResponseOutputItemDone(ServerEvent): 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"] type: Literal["response.output_item.done"]
response_id: str response_id: str
output_index: int output_index: int
@@ -293,6 +594,17 @@ class ResponseOutputItemDone(ServerEvent):
class ResponseContentPartAdded(ServerEvent): 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"] type: Literal["response.content_part.added"]
response_id: str response_id: str
item_id: str item_id: str
@@ -302,6 +614,17 @@ class ResponseContentPartAdded(ServerEvent):
class ResponseContentPartDone(ServerEvent): 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"] type: Literal["response.content_part.done"]
response_id: str response_id: str
item_id: str item_id: str
@@ -311,6 +634,17 @@ class ResponseContentPartDone(ServerEvent):
class ResponseTextDelta(ServerEvent): 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"] type: Literal["response.text.delta"]
response_id: str response_id: str
item_id: str item_id: str
@@ -320,6 +654,17 @@ class ResponseTextDelta(ServerEvent):
class ResponseTextDone(ServerEvent): 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"] type: Literal["response.text.done"]
response_id: str response_id: str
item_id: str item_id: str
@@ -329,6 +674,17 @@ class ResponseTextDone(ServerEvent):
class ResponseAudioTranscriptDelta(ServerEvent): 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"] type: Literal["response.audio_transcript.delta"]
response_id: str response_id: str
item_id: str item_id: str
@@ -338,6 +694,17 @@ class ResponseAudioTranscriptDelta(ServerEvent):
class ResponseAudioTranscriptDone(ServerEvent): 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"] type: Literal["response.audio_transcript.done"]
response_id: str response_id: str
item_id: str item_id: str
@@ -347,6 +714,17 @@ class ResponseAudioTranscriptDone(ServerEvent):
class ResponseAudioDelta(ServerEvent): 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"] type: Literal["response.audio.delta"]
response_id: str response_id: str
item_id: str item_id: str
@@ -356,6 +734,16 @@ class ResponseAudioDelta(ServerEvent):
class ResponseAudioDone(ServerEvent): 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"] type: Literal["response.audio.done"]
response_id: str response_id: str
item_id: str item_id: str
@@ -364,6 +752,17 @@ class ResponseAudioDone(ServerEvent):
class ResponseFunctionCallArgumentsDelta(ServerEvent): 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"] type: Literal["response.function_call_arguments.delta"]
response_id: str response_id: str
item_id: str item_id: str
@@ -373,6 +772,17 @@ class ResponseFunctionCallArgumentsDelta(ServerEvent):
class ResponseFunctionCallArgumentsDone(ServerEvent): 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"] type: Literal["response.function_call_arguments.done"]
response_id: str response_id: str
item_id: str item_id: str
@@ -382,38 +792,90 @@ class ResponseFunctionCallArgumentsDone(ServerEvent):
class InputAudioBufferSpeechStarted(ServerEvent): 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"] type: Literal["input_audio_buffer.speech_started"]
audio_start_ms: int audio_start_ms: int
item_id: str item_id: str
class InputAudioBufferSpeechStopped(ServerEvent): 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"] type: Literal["input_audio_buffer.speech_stopped"]
audio_end_ms: int audio_end_ms: int
item_id: str item_id: str
class InputAudioBufferCommitted(ServerEvent): 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"] type: Literal["input_audio_buffer.committed"]
previous_item_id: Optional[str] = None previous_item_id: Optional[str] = None
item_id: str item_id: str
class InputAudioBufferCleared(ServerEvent): 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"] type: Literal["input_audio_buffer.cleared"]
class ErrorEvent(ServerEvent): class ErrorEvent(ServerEvent):
"""Event indicating an error occurred.
Parameters:
type: Event type, always "error".
error: Error details.
"""
type: Literal["error"] type: Literal["error"]
error: RealtimeError error: RealtimeError
class RateLimitsUpdated(ServerEvent): 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"] type: Literal["rate_limits.updated"]
rate_limits: List[Dict[str, Any]] rate_limits: List[Dict[str, Any]]
class TokenDetails(BaseModel): 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.
"""
cached_tokens: Optional[int] = 0 cached_tokens: Optional[int] = 0
text_tokens: Optional[int] = 0 text_tokens: Optional[int] = 0
audio_tokens: Optional[int] = 0 audio_tokens: Optional[int] = 0
@@ -423,6 +885,16 @@ class TokenDetails(BaseModel):
class Usage(BaseModel): 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 total_tokens: int
input_tokens: int input_tokens: int
output_tokens: int output_tokens: int
@@ -431,6 +903,17 @@ class Usage(BaseModel):
class Response(BaseModel): 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 id: str
object: Literal["realtime.response"] object: Literal["realtime.response"]
status: Literal["completed", "in_progress", "incomplete", "cancelled", "failed"] status: Literal["completed", "in_progress", "incomplete", "cancelled", "failed"]
@@ -474,6 +957,17 @@ _server_event_types = {
def parse_server_event(str): 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: try:
event = json.loads(str) event = json.loads(str)
event_type = event["type"] event_type = event["type"]

View File

@@ -4,16 +4,31 @@
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
"""Custom frame types for OpenAI Realtime API integration."""
from dataclasses import dataclass from dataclasses import dataclass
from pipecat.frames.frames import DataFrame, FunctionCallResultFrame from pipecat.frames.frames import DataFrame, FunctionCallResultFrame
from pipecat.services.openai_realtime_beta.context import OpenAIRealtimeLLMContext
@dataclass @dataclass
class RealtimeMessagesUpdateFrame(DataFrame): class RealtimeMessagesUpdateFrame(DataFrame):
"""Frame indicating that the realtime context messages have been updated.
Parameters:
context: The updated OpenAI realtime LLM context.
"""
context: "OpenAIRealtimeLLMContext" context: "OpenAIRealtimeLLMContext"
@dataclass @dataclass
class RealtimeFunctionCallResultFrame(DataFrame): 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 result_frame: FunctionCallResultFrame

View File

@@ -4,6 +4,8 @@
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
"""OpenAI Realtime Beta LLM service implementation with WebSocket support."""
import base64 import base64
import json import json
import time import time
@@ -73,6 +75,15 @@ except ModuleNotFoundError as e:
@dataclass @dataclass
class CurrentAudioResponse: 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 item_id: str
content_index: int content_index: int
start_time_ms: int start_time_ms: int
@@ -80,6 +91,24 @@ class CurrentAudioResponse:
class OpenAIRealtimeBetaLLMService(LLMService): class OpenAIRealtimeBetaLLMService(LLMService):
"""OpenAI Realtime Beta LLM service providing real-time audio and text communication.
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.
Args:
api_key: OpenAI API key for authentication.
model: OpenAI model name. Defaults to "gpt-4o-realtime-preview-2025-06-03".
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.
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.
"""
# Overriding the default adapter to use the OpenAIRealtimeLLMAdapter one. # Overriding the default adapter to use the OpenAIRealtimeLLMAdapter one.
adapter_class = OpenAIRealtimeLLMAdapter adapter_class = OpenAIRealtimeLLMAdapter
@@ -125,12 +154,30 @@ class OpenAIRealtimeBetaLLMService(LLMService):
self._retrieve_conversation_item_futures = {} self._retrieve_conversation_item_futures = {}
def can_generate_metrics(self) -> bool: def can_generate_metrics(self) -> bool:
"""Check if the service can generate usage metrics.
Returns:
True if metrics generation is supported.
"""
return True return True
def set_audio_input_paused(self, paused: bool): 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 self._audio_input_paused = paused
async def retrieve_conversation_item(self, item_id: str): 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() future = self.get_event_loop().create_future()
retrieval_in_flight = False retrieval_in_flight = False
if not self._retrieve_conversation_item_futures.get(item_id): if not self._retrieve_conversation_item_futures.get(item_id):
@@ -154,14 +201,29 @@ class OpenAIRealtimeBetaLLMService(LLMService):
# #
async def start(self, frame: StartFrame): 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 super().start(frame)
await self._connect() await self._connect()
async def stop(self, frame: EndFrame): 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 super().stop(frame)
await self._disconnect() await self._disconnect()
async def cancel(self, frame: CancelFrame): 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 super().cancel(frame)
await self._disconnect() await self._disconnect()
@@ -247,6 +309,12 @@ class OpenAIRealtimeBetaLLMService(LLMService):
# #
async def process_frame(self, frame: Frame, direction: FrameDirection): 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.
"""
await super().process_frame(frame, direction) await super().process_frame(frame, direction)
if isinstance(frame, TranscriptionFrame): if isinstance(frame, TranscriptionFrame):
@@ -304,6 +372,11 @@ class OpenAIRealtimeBetaLLMService(LLMService):
# #
async def send_client_event(self, event: events.ClientEvent): 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)) await self._ws_send(event.model_dump(exclude_none=True))
async def _connect(self): async def _connect(self):
@@ -478,6 +551,11 @@ class OpenAIRealtimeBetaLLMService(LLMService):
pass pass
async def handle_evt_input_audio_transcription_completed(self, evt): 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) await self._call_event_handler("on_conversation_item_updated", evt.item_id, None)
if self._send_transcription_frames: if self._send_transcription_frames:
@@ -558,7 +636,9 @@ class OpenAIRealtimeBetaLLMService(LLMService):
await self.push_frame(UserStoppedSpeakingFrame()) await self.push_frame(UserStoppedSpeakingFrame())
async def _maybe_handle_evt_retrieve_conversation_item_error(self, evt: events.ErrorEvent): async def _maybe_handle_evt_retrieve_conversation_item_error(self, evt: events.ErrorEvent):
"""If the given error event is an error retrieving a conversation item: """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 - set an exception on the future that retrieve_conversation_item() is waiting on
- return true - return true
Otherwise: Otherwise:
@@ -605,8 +685,11 @@ class OpenAIRealtimeBetaLLMService(LLMService):
# #
async def reset_conversation(self): async def reset_conversation(self):
# Disconnect/reconnect is the safest way to start a new conversation. """Reset the conversation by disconnecting and reconnecting.
# Note that this will fail if called from the receive task.
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") logger.debug("Resetting conversation")
await self._disconnect() await self._disconnect()
if self._context: if self._context:
@@ -654,22 +737,19 @@ class OpenAIRealtimeBetaLLMService(LLMService):
user_params: LLMUserAggregatorParams = LLMUserAggregatorParams(), user_params: LLMUserAggregatorParams = LLMUserAggregatorParams(),
assistant_params: LLMAssistantAggregatorParams = LLMAssistantAggregatorParams(), assistant_params: LLMAssistantAggregatorParams = LLMAssistantAggregatorParams(),
) -> OpenAIContextAggregatorPair: ) -> OpenAIContextAggregatorPair:
"""Create an instance of OpenAIContextAggregatorPair from an """Create an instance of OpenAIContextAggregatorPair from an OpenAILLMContext.
OpenAILLMContext. Constructor keyword arguments for both the user and
assistant aggregators can be provided. Constructor keyword arguments for both the user and assistant aggregators can be provided.
Args: Args:
context (OpenAILLMContext): The LLM context. context: The LLM context.
user_params (LLMUserAggregatorParams, optional): User aggregator user_params: User aggregator parameters.
parameters. assistant_params: Assistant aggregator parameters.
assistant_params (LLMAssistantAggregatorParams, optional): User
aggregator parameters.
Returns: Returns:
OpenAIContextAggregatorPair: A pair of context aggregators, one for OpenAIContextAggregatorPair: A pair of context aggregators, one for
the user and one for the assistant, encapsulated in an the user and one for the assistant, encapsulated in an
OpenAIContextAggregatorPair. OpenAIContextAggregatorPair.
""" """
context.set_llm_adapter(self.get_llm_adapter()) context.set_llm_adapter(self.get_llm_adapter())