2424from pyrit .prompt_target .common .target_capabilities import TargetCapabilities
2525from pyrit .prompt_target .common .target_configuration import TargetConfiguration
2626from pyrit .prompt_target .common .utils import limit_requests_per_minute
27+ from pyrit .prompt_target .openai ._openai_realtime_event_router import (
28+ _OpenAIRealtimeEventKind ,
29+ _OpenAIRealtimeEventRouter ,
30+ )
2731from pyrit .prompt_target .openai ._openai_realtime_streaming_session import (
2832 _OpenAIRealtimeStreamingSession ,
2933)
@@ -575,6 +579,7 @@ async def receive_events_async(self, conversation_id: str) -> RealtimeTargetResu
575579 connection = self ._get_connection (conversation_id = conversation_id )
576580
577581 result = RealtimeTargetResult ()
582+ audio_buffer = bytearray ()
578583 audio_done_received = False
579584 current_turn_event_count = 0
580585 grace_period_sec = 1.0 # Wait 1 second after audio.done before soft-finishing
@@ -595,7 +600,7 @@ async def receive_events_async(self, conversation_id: str) -> RealtimeTargetResu
595600 if audio_done_received :
596601 logger .warning (
597602 f"Soft-finishing: No response.done { grace_period_sec } s after audio.done. "
598- f"Audio bytes: { len (result . audio_bytes )} "
603+ f"Audio bytes: { len (audio_buffer )} "
599604 )
600605 break
601606 # Should not happen if timeout is None, but re-raise if it does
@@ -606,22 +611,30 @@ async def receive_events_async(self, conversation_id: str) -> RealtimeTargetResu
606611 break
607612 except Exception as conn_err :
608613 # Handle websockets connection errors as soft-finish if we have audio
609- if "ConnectionClosed" in str (type (conn_err ).__name__ ) and result . audio_bytes :
614+ if "ConnectionClosed" in str (type (conn_err ).__name__ ) and audio_buffer :
610615 logger .warning (
611616 f"Connection closed without response.done (likely API issue). "
612- f"Audio bytes received: { len (result . audio_bytes )} . Soft-finishing."
617+ f"Audio bytes received: { len (audio_buffer )} . Soft-finishing."
613618 )
614619 break
615620 # Re-raise if not a connection close or no audio received
616621 raise
617622
618623 event_type = event .type
624+ event_kind = _OpenAIRealtimeEventRouter .classify_event (event_type )
619625 current_turn_event_count += 1
620626 logger .debug (f"Processing event type: { event_type } " )
621-
622- if event_type == "response.done" :
627+ audio_size_before = len (audio_buffer )
628+ _OpenAIRealtimeEventRouter .collect_response_delta (
629+ event = event ,
630+ event_kind = event_kind ,
631+ audio_buffer = audio_buffer ,
632+ transcripts = result .transcripts ,
633+ )
634+
635+ if event_kind is _OpenAIRealtimeEventKind .RESPONSE_DONE :
623636 self ._handle_response_done_event (event = event , result = result )
624- if result . audio_bytes or current_turn_event_count > 1 :
637+ if audio_buffer or current_turn_event_count > 1 :
625638 # Legitimate response.done: either we have audio, or other events
626639 # (e.g. response.created) preceded it, confirming it belongs to this turn.
627640 logger .debug ("Received response.done - finishing normally" )
@@ -635,53 +648,27 @@ async def receive_events_async(self, conversation_id: str) -> RealtimeTargetResu
635648 "likely a stale event from a prior turn's soft-finish. Skipping."
636649 )
637650
638- elif event_type == "error" :
651+ elif event_kind is _OpenAIRealtimeEventKind . ERROR :
639652 error_message = event .error .message if hasattr (event .error , "message" ) else str (event .error )
640653 error_type = event .error .type if hasattr (event .error , "type" ) else "unknown"
641654 logger .error (f"Received 'error' event: [{ error_type } ] { error_message } " )
642655 raise RuntimeError (f"Server error: [{ error_type } ] { error_message } " )
643656
644- elif event_type in ["response.audio.delta" , "response.output_audio.delta" ]:
645- audio_data = base64 .b64decode (event .delta )
646- result .audio_bytes += audio_data
647- logger .debug (f"Decoded { len (audio_data )} bytes of audio data" )
657+ elif event_kind is _OpenAIRealtimeEventKind .AUDIO_DELTA :
658+ logger .debug (f"Decoded { len (audio_buffer ) - audio_size_before } bytes of audio data" )
648659
649- elif event_type in [ "response.audio.done" , "response.output_audio.done" ] :
660+ elif event_kind is _OpenAIRealtimeEventKind . AUDIO_DONE :
650661 logger .debug (f"Received audio.done - will soft-finish in { grace_period_sec } s if no response.done" )
651662 audio_done_received = True
652663
653- elif event_type in ["response.audio_transcript.delta" , "response.output_audio_transcript.delta" ]:
654- # Capture transcript deltas as they arrive (needed when response.done never comes)
655- if hasattr (event , "delta" ) and event .delta :
656- result .transcripts .append (event .delta )
664+ elif event_kind is _OpenAIRealtimeEventKind .TRANSCRIPT_DELTA :
665+ if getattr (event , "delta" , "" ):
657666 logger .debug (f"Captured transcript delta: { event .delta [:50 ]} ..." )
658667
659- elif event_type in [ "response.output_text.done" ] :
668+ elif event_kind is _OpenAIRealtimeEventKind . OUTPUT_TEXT_DONE :
660669 logger .debug ("Received text.done" )
661670
662- # Handle lifecycle events that we can safely log
663- elif event_type in [
664- "session.created" ,
665- "session.updated" ,
666- "conversation.created" ,
667- "conversation.item.created" ,
668- "conversation.item.added" ,
669- "conversation.item.done" ,
670- "input_audio_buffer.committed" ,
671- "input_audio_buffer.speech_started" ,
672- "input_audio_buffer.speech_stopped" ,
673- "conversation.item.input_audio_transcription.completed" ,
674- "response.created" ,
675- "response.output_item.added" ,
676- "response.output_item.created" ,
677- "response.output_item.done" ,
678- "response.content_part.added" ,
679- "response.content_part.done" ,
680- "response.audio_transcript.done" ,
681- "response.output_audio_transcript.done" ,
682- "response.output_text.delta" ,
683- "rate_limits.updated" ,
684- ]:
671+ elif _OpenAIRealtimeEventRouter .is_lifecycle_event (event_kind ):
685672 logger .debug (f"Lifecycle event '{ event_type } '" )
686673
687674 else :
@@ -691,6 +678,7 @@ async def receive_events_async(self, conversation_id: str) -> RealtimeTargetResu
691678 logger .error (f"An unexpected error occurred for conversation { conversation_id } : { e } " )
692679 raise
693680
681+ result .audio_bytes = bytes (audio_buffer )
694682 logger .debug (
695683 f"Completed receive_events with { len (result .transcripts )} transcripts "
696684 f"and { len (result .audio_bytes )} bytes of audio"
0 commit comments