39
39
RealtimeModelAudioInterruptedEvent ,
40
40
RealtimeModelErrorEvent ,
41
41
RealtimeModelEvent ,
42
+ RealtimeModelExceptionEvent ,
42
43
RealtimeModelInputAudioTranscriptionCompletedEvent ,
43
44
RealtimeModelItemDeletedEvent ,
44
45
RealtimeModelItemUpdatedEvent ,
@@ -130,48 +131,84 @@ async def _listen_for_messages(self):
130
131
131
132
try :
132
133
async for message in self ._websocket :
133
- parsed = json .loads (message )
134
- await self ._handle_ws_event (parsed )
134
+ try :
135
+ parsed = json .loads (message )
136
+ await self ._handle_ws_event (parsed )
137
+ except json .JSONDecodeError as e :
138
+ await self ._emit_event (
139
+ RealtimeModelExceptionEvent (
140
+ exception = e , context = "Failed to parse WebSocket message as JSON"
141
+ )
142
+ )
143
+ except Exception as e :
144
+ await self ._emit_event (
145
+ RealtimeModelExceptionEvent (
146
+ exception = e , context = "Error handling WebSocket event"
147
+ )
148
+ )
135
149
136
- except websockets .exceptions .ConnectionClosed :
137
- # TODO connection closed handling (event, cleanup)
138
- logger .warning ("WebSocket connection closed" )
150
+ except websockets .exceptions .ConnectionClosedOK :
151
+ # Normal connection closure - no exception event needed
152
+ logger .info ("WebSocket connection closed normally" )
153
+ except websockets .exceptions .ConnectionClosed as e :
154
+ await self ._emit_event (
155
+ RealtimeModelExceptionEvent (
156
+ exception = e , context = "WebSocket connection closed unexpectedly"
157
+ )
158
+ )
139
159
except Exception as e :
140
- logger .error (f"WebSocket error: { e } " )
160
+ await self ._emit_event (
161
+ RealtimeModelExceptionEvent (
162
+ exception = e , context = "WebSocket error in message listener"
163
+ )
164
+ )
141
165
142
166
async def send_event (self , event : RealtimeClientMessage ) -> None :
143
167
"""Send an event to the model."""
144
168
assert self ._websocket is not None , "Not connected"
145
- converted_event = {
146
- "type" : event ["type" ],
147
- }
148
169
149
- converted_event .update (event .get ("other_data" , {}))
170
+ try :
171
+ converted_event = {
172
+ "type" : event ["type" ],
173
+ }
150
174
151
- await self ._websocket .send (json .dumps (converted_event ))
175
+ converted_event .update (event .get ("other_data" , {}))
176
+
177
+ await self ._websocket .send (json .dumps (converted_event ))
178
+ except Exception as e :
179
+ await self ._emit_event (
180
+ RealtimeModelExceptionEvent (
181
+ exception = e , context = f"Failed to send event: { event .get ('type' , 'unknown' )} "
182
+ )
183
+ )
152
184
153
185
async def send_message (
154
186
self , message : RealtimeUserInput , other_event_data : dict [str , Any ] | None = None
155
187
) -> None :
156
188
"""Send a message to the model."""
157
- message = (
158
- message
159
- if isinstance (message , dict )
160
- else {
161
- "type" : "message" ,
162
- "role" : "user" ,
163
- "content" : [{"type" : "input_text" , "text" : message }],
189
+ try :
190
+ message = (
191
+ message
192
+ if isinstance (message , dict )
193
+ else {
194
+ "type" : "message" ,
195
+ "role" : "user" ,
196
+ "content" : [{"type" : "input_text" , "text" : message }],
197
+ }
198
+ )
199
+ other_data = {
200
+ "item" : message ,
164
201
}
165
- )
166
- other_data = {
167
- "item" : message ,
168
- }
169
- if other_event_data :
170
- other_data .update (other_event_data )
202
+ if other_event_data :
203
+ other_data .update (other_event_data )
171
204
172
- await self .send_event ({"type" : "conversation.item.create" , "other_data" : other_data })
205
+ await self .send_event ({"type" : "conversation.item.create" , "other_data" : other_data })
173
206
174
- await self .send_event ({"type" : "response.create" })
207
+ await self .send_event ({"type" : "response.create" })
208
+ except Exception as e :
209
+ await self ._emit_event (
210
+ RealtimeModelExceptionEvent (exception = e , context = "Failed to send message" )
211
+ )
175
212
176
213
async def send_audio (self , audio : bytes , * , commit : bool = False ) -> None :
177
214
"""Send a raw audio chunk to the model.
@@ -182,17 +219,23 @@ async def send_audio(self, audio: bytes, *, commit: bool = False) -> None:
182
219
detection, this can be used to indicate the turn is completed.
183
220
"""
184
221
assert self ._websocket is not None , "Not connected"
185
- base64_audio = base64 .b64encode (audio ).decode ("utf-8" )
186
- await self .send_event (
187
- {
188
- "type" : "input_audio_buffer.append" ,
189
- "other_data" : {
190
- "audio" : base64_audio ,
191
- },
192
- }
193
- )
194
- if commit :
195
- await self .send_event ({"type" : "input_audio_buffer.commit" })
222
+
223
+ try :
224
+ base64_audio = base64 .b64encode (audio ).decode ("utf-8" )
225
+ await self .send_event (
226
+ {
227
+ "type" : "input_audio_buffer.append" ,
228
+ "other_data" : {
229
+ "audio" : base64_audio ,
230
+ },
231
+ }
232
+ )
233
+ if commit :
234
+ await self .send_event ({"type" : "input_audio_buffer.commit" })
235
+ except Exception as e :
236
+ await self ._emit_event (
237
+ RealtimeModelExceptionEvent (exception = e , context = "Failed to send audio" )
238
+ )
196
239
197
240
async def send_tool_output (
198
241
self , tool_call : RealtimeModelToolCallEvent , output : str , start_response : bool
@@ -342,8 +385,13 @@ async def _handle_ws_event(self, event: dict[str, Any]):
342
385
OpenAIRealtimeServerEvent
343
386
).validate_python (event )
344
387
except Exception as e :
345
- logger .error (f"Invalid event: { event } - { e } " )
346
- # await self._emit_event(RealtimeModelErrorEvent(error=f"Invalid event: {event} - {e}"))
388
+ event_type = event .get ("type" , "unknown" ) if isinstance (event , dict ) else "unknown"
389
+ await self ._emit_event (
390
+ RealtimeModelExceptionEvent (
391
+ exception = e ,
392
+ context = f"Failed to validate server event: { event_type } " ,
393
+ )
394
+ )
347
395
return
348
396
349
397
if parsed .type == "response.audio.delta" :
0 commit comments