Skip to content

Commit 201179c

Browse files
committed
[4/n] Transport interface + events
1 parent 7dcc37c commit 201179c

File tree

3 files changed

+267
-0
lines changed

3 files changed

+267
-0
lines changed

src/agents/realtime/transport.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import abc
2+
from typing import Any, Literal, Union
3+
4+
from typing_extensions import NotRequired, TypeAlias, TypedDict
5+
6+
from .config import APIKeyOrKeyFunc, RealtimeClientMessage, RealtimeSessionConfig, RealtimeUserInput
7+
from .transport_events import RealtimeTransportEvent, RealtimeTransportToolCallEvent
8+
9+
RealtimeModelName: TypeAlias = Union[
10+
Literal[
11+
"gpt-4o-realtime-preview",
12+
"gpt-4o-mini-realtime-preview",
13+
"gpt-4o-realtime-preview-2025-06-03",
14+
"gpt-4o-realtime-preview-2024-12-17",
15+
"gpt-4o-realtime-preview-2024-10-01",
16+
"gpt-4o-mini-realtime-preview-2024-12-17",
17+
],
18+
str,
19+
]
20+
"""The name of a realtime model."""
21+
22+
23+
class RealtimeTransportListener(abc.ABC):
24+
"""A listener for realtime transport events."""
25+
26+
@abc.abstractmethod
27+
async def on_event(self, event: RealtimeTransportEvent) -> None:
28+
"""Called when an event is emitted by the realtime transport."""
29+
pass
30+
31+
32+
class RealtimeTransportConnectionOptions(TypedDict):
33+
"""Options for connecting to a realtime transport."""
34+
35+
api_key: NotRequired[APIKeyOrKeyFunc]
36+
"""The API key to use for the transport. If unset, the transport will attempt to use the
37+
`OPENAI_API_KEY` environment variable.
38+
"""
39+
40+
model: NotRequired[str]
41+
"""The model to use."""
42+
43+
url: NotRequired[str]
44+
"""The URL to use for the transport. If unset, the transport will use the default OpenAI
45+
WebSocket URL.
46+
"""
47+
48+
initial_session_config: NotRequired[RealtimeSessionConfig]
49+
50+
51+
class RealtimeSessionTransport(abc.ABC):
52+
"""A transport layer for realtime sessions."""
53+
54+
@abc.abstractmethod
55+
async def connect(self, options: RealtimeTransportConnectionOptions) -> None:
56+
"""Establish a connection to the model and keep it alive."""
57+
pass
58+
59+
@abc.abstractmethod
60+
def add_listener(self, listener: RealtimeTransportListener) -> None:
61+
"""Add a listener to the transport."""
62+
pass
63+
64+
@abc.abstractmethod
65+
async def remove_listener(self, listener: RealtimeTransportListener) -> None:
66+
"""Remove a listener from the transport."""
67+
pass
68+
69+
@abc.abstractmethod
70+
async def send_event(self, event: RealtimeClientMessage) -> None:
71+
"""Send an event to the model."""
72+
pass
73+
74+
@abc.abstractmethod
75+
async def send_message(
76+
self, message: RealtimeUserInput, other_event_data: dict[str, Any] | None = None
77+
) -> None:
78+
"""Send a message to the model."""
79+
pass
80+
81+
@abc.abstractmethod
82+
async def send_audio(self, audio: bytes, *, commit: bool = False) -> None:
83+
"""Send a raw audio chunk to the model.
84+
85+
Args:
86+
audio: The audio data to send.
87+
commit: Whether to commit the audio buffer to the model. If the model does not do turn
88+
detection, this can be used to indicate the turn is completed.
89+
"""
90+
pass
91+
92+
@abc.abstractmethod
93+
async def send_tool_output(
94+
self, tool_call: RealtimeTransportToolCallEvent, output: str, start_response: bool
95+
) -> None:
96+
"""Send tool output to the model."""
97+
pass
98+
99+
@abc.abstractmethod
100+
async def interrupt(self) -> None:
101+
"""Interrupt the model. For example, could be triggered by a guardrail."""
102+
pass
103+
104+
@abc.abstractmethod
105+
async def close(self) -> None:
106+
"""Close the session."""
107+
pass
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import Any, Literal, TypeAlias, Union
5+
6+
from .items import RealtimeItem
7+
8+
RealtimeConnectionStatus: TypeAlias = Literal["connecting", "connected", "disconnected"]
9+
10+
11+
@dataclass
12+
class RealtimeTransportErrorEvent:
13+
"""Represents a transport‑layer error."""
14+
15+
error: Any
16+
17+
type: Literal["error"] = "error"
18+
19+
20+
@dataclass
21+
class RealtimeTransportToolCallEvent:
22+
"""Model attempted a tool/function call."""
23+
24+
name: str
25+
call_id: str
26+
arguments: str
27+
28+
id: str | None = None
29+
previous_item_id: str | None = None
30+
31+
type: Literal["function_call"] = "function_call"
32+
33+
34+
@dataclass
35+
class RealtimeTransportAudioEvent:
36+
"""Raw audio bytes emitted by the model."""
37+
38+
data: bytes
39+
response_id: str
40+
41+
type: Literal["audio"] = "audio"
42+
43+
44+
@dataclass
45+
class RealtimeTransportAudioInterruptedEvent:
46+
"""Audio interrupted."""
47+
48+
type: Literal["audio_interrupted"] = "audio_interrupted"
49+
50+
51+
@dataclass
52+
class RealtimeTransportAudioDoneEvent:
53+
"""Audio done."""
54+
55+
type: Literal["audio_done"] = "audio_done"
56+
57+
58+
@dataclass
59+
class RealtimeTransportInputAudioTranscriptionCompletedEvent:
60+
"""Input audio transcription completed."""
61+
62+
item_id: str
63+
transcript: str
64+
65+
type: Literal["conversation.item.input_audio_transcription.completed"] = (
66+
"conversation.item.input_audio_transcription.completed"
67+
)
68+
69+
70+
@dataclass
71+
class RealtimeTransportTranscriptDelta:
72+
"""Partial transcript update."""
73+
74+
item_id: str
75+
delta: str
76+
response_id: str
77+
78+
type: Literal["transcript_delta"] = "transcript_delta"
79+
80+
81+
@dataclass
82+
class RealtimeTransportItemUpdatedEvent:
83+
"""Item added to the history or updated."""
84+
85+
item: RealtimeItem
86+
87+
type: Literal["item_updated"] = "item_updated"
88+
89+
90+
@dataclass
91+
class RealtimeTransportItemDeletedEvent:
92+
"""Item deleted from the history."""
93+
94+
item_id: str
95+
96+
type: Literal["item_deleted"] = "item_deleted"
97+
98+
99+
@dataclass
100+
class RealtimeTransportConnectionStatusEvent:
101+
"""Connection status changed."""
102+
103+
status: RealtimeConnectionStatus
104+
105+
type: Literal["connection_status"] = "connection_status"
106+
107+
108+
@dataclass
109+
class RealtimeTransportTurnStartedEvent:
110+
"""Triggered when the model starts generating a response for a turn."""
111+
112+
type: Literal["turn_started"] = "turn_started"
113+
114+
115+
@dataclass
116+
class RealtimeTransportTurnEndedEvent:
117+
"""Triggered when the model finishes generating a response for a turn."""
118+
119+
type: Literal["turn_ended"] = "turn_ended"
120+
121+
122+
@dataclass
123+
class RealtimeTransportOtherEvent:
124+
"""Used as a catchall for vendor-specific events."""
125+
126+
data: Any
127+
128+
type: Literal["other"] = "other"
129+
130+
131+
# TODO (rm) Add usage events
132+
133+
134+
RealtimeTransportEvent: TypeAlias = Union[
135+
RealtimeTransportErrorEvent,
136+
RealtimeTransportToolCallEvent,
137+
RealtimeTransportAudioEvent,
138+
RealtimeTransportAudioInterruptedEvent,
139+
RealtimeTransportAudioDoneEvent,
140+
RealtimeTransportInputAudioTranscriptionCompletedEvent,
141+
RealtimeTransportTranscriptDelta,
142+
RealtimeTransportItemUpdatedEvent,
143+
RealtimeTransportItemDeletedEvent,
144+
RealtimeTransportConnectionStatusEvent,
145+
RealtimeTransportTurnStartedEvent,
146+
RealtimeTransportTurnEndedEvent,
147+
RealtimeTransportOtherEvent,
148+
]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from typing import get_args
2+
3+
from agents.realtime.transport_events import RealtimeTransportEvent
4+
5+
6+
def test_all_events_have_type() -> None:
7+
"""Test that all events have a type."""
8+
events = get_args(RealtimeTransportEvent)
9+
assert len(events) > 0
10+
for event in events:
11+
assert event.type is not None
12+
assert isinstance(event.type, str)

0 commit comments

Comments
 (0)