From 54b1569966db31fb2824e9315a7760b472d9dc7c Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 14 Jul 2025 17:53:45 -0400 Subject: [PATCH] Realtime: update model to have a single send_event method --- src/agents/realtime/config.py | 36 ++--- src/agents/realtime/model.py | 39 +---- src/agents/realtime/model_events.py | 4 +- src/agents/realtime/model_inputs.py | 90 ++++++++++++ src/agents/realtime/openai_realtime.py | 192 +++++++++++++++---------- src/agents/realtime/session.py | 24 +++- src/agents/voice/models/openai_stt.py | 2 +- tests/realtime/test_session.py | 31 ++-- tests/realtime/test_tracing.py | 46 +++--- tests/voice/test_openai_stt.py | 4 +- 10 files changed, 295 insertions(+), 173 deletions(-) create mode 100644 src/agents/realtime/model_inputs.py diff --git a/src/agents/realtime/config.py b/src/agents/realtime/config.py index 9333a9cca..7f874cfb0 100644 --- a/src/agents/realtime/config.py +++ b/src/agents/realtime/config.py @@ -26,29 +26,17 @@ """The name of a realtime model.""" +RealtimeAudioFormat: TypeAlias = Union[Literal["pcm16", "g711_ulaw", "g711_alaw"], str] + + class RealtimeClientMessage(TypedDict): + """A raw message to be sent to the model.""" + type: str # explicitly required other_data: NotRequired[dict[str, Any]] """Merged into the message body.""" -class RealtimeUserInputText(TypedDict): - type: Literal["input_text"] - text: str - - -class RealtimeUserInputMessage(TypedDict): - type: Literal["message"] - role: Literal["user"] - content: list[RealtimeUserInputText] - - -RealtimeUserInput: TypeAlias = Union[str, RealtimeUserInputMessage] - - -RealtimeAudioFormat: TypeAlias = Union[Literal["pcm16", "g711_ulaw", "g711_alaw"], str] - - class RealtimeInputAudioTranscriptionConfig(TypedDict): language: NotRequired[str] model: NotRequired[Literal["gpt-4o-transcribe", "gpt-4o-mini-transcribe", "whisper-1"] | str] @@ -124,3 +112,17 @@ class RealtimeRunConfig(TypedDict): """Whether tracing is disabled for this run.""" # TODO (rm) Add history audio storage config + + +class RealtimeUserInputText(TypedDict): + type: Literal["input_text"] + text: str + + +class RealtimeUserInputMessage(TypedDict): + type: Literal["message"] + role: Literal["user"] + content: list[RealtimeUserInputText] + + +RealtimeUserInput: TypeAlias = Union[str, RealtimeUserInputMessage] diff --git a/src/agents/realtime/model.py b/src/agents/realtime/model.py index abb3a1eac..e279ecc95 100644 --- a/src/agents/realtime/model.py +++ b/src/agents/realtime/model.py @@ -1,17 +1,16 @@ from __future__ import annotations import abc -from typing import Any, Callable +from typing import Callable from typing_extensions import NotRequired, TypedDict from ..util._types import MaybeAwaitable from .config import ( - RealtimeClientMessage, RealtimeSessionModelSettings, - RealtimeUserInput, ) -from .model_events import RealtimeModelEvent, RealtimeModelToolCallEvent +from .model_events import RealtimeModelEvent +from .model_inputs import RealtimeModelSendEvent class RealtimeModelListener(abc.ABC): @@ -60,40 +59,10 @@ def remove_listener(self, listener: RealtimeModelListener) -> None: pass @abc.abstractmethod - async def send_event(self, event: RealtimeClientMessage) -> None: + async def send_event(self, event: RealtimeModelSendEvent) -> None: """Send an event to the model.""" pass - @abc.abstractmethod - async def send_message( - self, message: RealtimeUserInput, other_event_data: dict[str, Any] | None = None - ) -> None: - """Send a message to the model.""" - pass - - @abc.abstractmethod - async def send_audio(self, audio: bytes, *, commit: bool = False) -> None: - """Send a raw audio chunk to the model. - - Args: - audio: The audio data to send. - commit: Whether to commit the audio buffer to the model. If the model does not do turn - detection, this can be used to indicate the turn is completed. - """ - pass - - @abc.abstractmethod - async def send_tool_output( - self, tool_call: RealtimeModelToolCallEvent, output: str, start_response: bool - ) -> None: - """Send tool output to the model.""" - pass - - @abc.abstractmethod - async def interrupt(self) -> None: - """Interrupt the model. For example, could be triggered by a guardrail.""" - pass - @abc.abstractmethod async def close(self) -> None: """Close the session.""" diff --git a/src/agents/realtime/model_events.py b/src/agents/realtime/model_events.py index de1ad5f54..3a158ef4e 100644 --- a/src/agents/realtime/model_events.py +++ b/src/agents/realtime/model_events.py @@ -64,9 +64,7 @@ class RealtimeModelInputAudioTranscriptionCompletedEvent: item_id: str transcript: str - type: Literal["conversation.item.input_audio_transcription.completed"] = ( - "conversation.item.input_audio_transcription.completed" - ) + type: Literal["input_audio_transcription_completed"] = "input_audio_transcription_completed" @dataclass diff --git a/src/agents/realtime/model_inputs.py b/src/agents/realtime/model_inputs.py new file mode 100644 index 000000000..eb8e8220d --- /dev/null +++ b/src/agents/realtime/model_inputs.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Literal, Union + +from typing_extensions import NotRequired, TypeAlias, TypedDict + +from .model_events import RealtimeModelToolCallEvent + + +class RealtimeModelRawClientMessage(TypedDict): + """A raw message to be sent to the model.""" + + type: str # explicitly required + other_data: NotRequired[dict[str, Any]] + """Merged into the message body.""" + + +class RealtimeModelInputTextContent(TypedDict): + """A piece of text to be sent to the model.""" + + type: Literal["input_text"] + text: str + + +class RealtimeModelUserInputMessage(TypedDict): + """A message to be sent to the model.""" + + type: Literal["message"] + role: Literal["user"] + content: list[RealtimeModelInputTextContent] + + +RealtimeModelUserInput: TypeAlias = Union[str, RealtimeModelUserInputMessage] +"""A user input to be sent to the model.""" + + +# Model messages + + +@dataclass +class RealtimeModelSendRawMessage: + """Send a raw message to the model.""" + + message: RealtimeModelRawClientMessage + """The message to send.""" + + +@dataclass +class RealtimeModelSendUserInput: + """Send a user input to the model.""" + + user_input: RealtimeModelUserInput + """The user input to send.""" + + +@dataclass +class RealtimeModelSendAudio: + """Send audio to the model.""" + + audio: bytes + commit: bool = False + + +@dataclass +class RealtimeModelSendToolOutput: + """Send tool output to the model.""" + + tool_call: RealtimeModelToolCallEvent + """The tool call to send.""" + + output: str + """The output to send.""" + + start_response: bool + """Whether to start a response.""" + + +@dataclass +class RealtimeModelSendInterrupt: + """Send an interrupt to the model.""" + + +RealtimeModelSendEvent: TypeAlias = Union[ + RealtimeModelSendRawMessage, + RealtimeModelSendUserInput, + RealtimeModelSendAudio, + RealtimeModelSendToolOutput, + RealtimeModelSendInterrupt, +] diff --git a/src/agents/realtime/openai_realtime.py b/src/agents/realtime/openai_realtime.py index e239fd003..b73ca8503 100644 --- a/src/agents/realtime/openai_realtime.py +++ b/src/agents/realtime/openai_realtime.py @@ -15,6 +15,7 @@ ) from openai.types.beta.realtime.response_audio_delta_event import ResponseAudioDeltaEvent from pydantic import TypeAdapter +from typing_extensions import assert_never from websockets.asyncio.client import ClientConnection from agents.util._types import MaybeAwaitable @@ -22,10 +23,8 @@ from ..exceptions import UserError from ..logger import logger from .config import ( - RealtimeClientMessage, RealtimeModelTracingConfig, RealtimeSessionModelSettings, - RealtimeUserInput, ) from .items import RealtimeMessageItem, RealtimeToolCallItem from .model import ( @@ -48,6 +47,14 @@ RealtimeModelTurnEndedEvent, RealtimeModelTurnStartedEvent, ) +from .model_inputs import ( + RealtimeModelSendAudio, + RealtimeModelSendEvent, + RealtimeModelSendInterrupt, + RealtimeModelSendRawMessage, + RealtimeModelSendToolOutput, + RealtimeModelSendUserInput, +) async def get_api_key(key: str | Callable[[], MaybeAwaitable[str]] | None) -> str | None: @@ -109,8 +116,13 @@ async def _send_tracing_config( ) -> None: """Update tracing configuration via session.update event.""" if tracing_config is not None: - await self.send_event( - {"type": "session.update", "other_data": {"session": {"tracing": tracing_config}}} + await self._send_raw_message( + RealtimeModelSendRawMessage( + message={ + "type": "session.update", + "other_data": {"session": {"tracing": tracing_config}}, + } + ) ) def add_listener(self, listener: RealtimeModelListener) -> None: @@ -163,113 +175,131 @@ async def _listen_for_messages(self): ) ) - async def send_event(self, event: RealtimeClientMessage) -> None: + async def send_event(self, event: RealtimeModelSendEvent) -> None: """Send an event to the model.""" + if isinstance(event, RealtimeModelSendRawMessage): + await self._send_raw_message(event) + elif isinstance(event, RealtimeModelSendUserInput): + await self._send_user_input(event) + elif isinstance(event, RealtimeModelSendAudio): + await self._send_audio(event) + elif isinstance(event, RealtimeModelSendToolOutput): + await self._send_tool_output(event) + elif isinstance(event, RealtimeModelSendInterrupt): + await self._send_interrupt(event) + else: + assert_never(event) + raise ValueError(f"Unknown event type: {type(event)}") + + async def _send_raw_message(self, event: RealtimeModelSendRawMessage) -> None: + """Send a raw message to the model.""" assert self._websocket is not None, "Not connected" try: converted_event = { - "type": event["type"], + "type": event.message["type"], } - converted_event.update(event.get("other_data", {})) + converted_event.update(event.message.get("other_data", {})) await self._websocket.send(json.dumps(converted_event)) except Exception as e: await self._emit_event( RealtimeModelExceptionEvent( - exception=e, context=f"Failed to send event: {event.get('type', 'unknown')}" + exception=e, + context=f"Failed to send event: {event.message.get('type', 'unknown')}", ) ) - async def send_message( - self, message: RealtimeUserInput, other_event_data: dict[str, Any] | None = None - ) -> None: - """Send a message to the model.""" + async def _send_user_input(self, event: RealtimeModelSendUserInput) -> None: + """Send a user input to the model.""" try: message = ( - message - if isinstance(message, dict) + event.user_input + if isinstance(event.user_input, dict) else { "type": "message", "role": "user", - "content": [{"type": "input_text", "text": message}], + "content": [{"type": "input_text", "text": event.user_input}], } ) other_data = { "item": message, } - if other_event_data: - other_data.update(other_event_data) - - await self.send_event({"type": "conversation.item.create", "other_data": other_data}) - await self.send_event({"type": "response.create"}) + await self._send_raw_message( + RealtimeModelSendRawMessage( + message={"type": "conversation.item.create", "other_data": other_data} + ) + ) + await self._send_raw_message( + RealtimeModelSendRawMessage(message={"type": "response.create"}) + ) except Exception as e: await self._emit_event( RealtimeModelExceptionEvent(exception=e, context="Failed to send message") ) - async def send_audio(self, audio: bytes, *, commit: bool = False) -> None: - """Send a raw audio chunk to the model. - - Args: - audio: The audio data to send. - commit: Whether to commit the audio buffer to the model. If the model does not do turn - detection, this can be used to indicate the turn is completed. - """ + async def _send_audio(self, event: RealtimeModelSendAudio) -> None: + """Send audio to the model.""" assert self._websocket is not None, "Not connected" try: - base64_audio = base64.b64encode(audio).decode("utf-8") - await self.send_event( - { - "type": "input_audio_buffer.append", - "other_data": { - "audio": base64_audio, - }, - } + base64_audio = base64.b64encode(event.audio).decode("utf-8") + await self._send_raw_message( + RealtimeModelSendRawMessage( + message={ + "type": "input_audio_buffer.append", + "other_data": { + "audio": base64_audio, + }, + } + ) ) - if commit: - await self.send_event({"type": "input_audio_buffer.commit"}) + if event.commit: + await self._send_raw_message( + RealtimeModelSendRawMessage(message={"type": "input_audio_buffer.commit"}) + ) except Exception as e: await self._emit_event( RealtimeModelExceptionEvent(exception=e, context="Failed to send audio") ) - async def send_tool_output( - self, tool_call: RealtimeModelToolCallEvent, output: str, start_response: bool - ) -> None: + async def _send_tool_output(self, event: RealtimeModelSendToolOutput) -> None: """Send tool output to the model.""" - await self.send_event( - { - "type": "conversation.item.create", - "other_data": { - "item": { - "type": "function_call_output", - "output": output, - "call_id": tool_call.id, + await self._send_raw_message( + RealtimeModelSendRawMessage( + message={ + "type": "conversation.item.create", + "other_data": { + "item": { + "type": "function_call_output", + "output": event.output, + "call_id": event.tool_call.id, + }, }, - }, - } + } + ) ) tool_item = RealtimeToolCallItem( - item_id=tool_call.id or "", - previous_item_id=tool_call.previous_item_id, + item_id=event.tool_call.id or "", + previous_item_id=event.tool_call.previous_item_id, type="function_call", status="completed", - arguments=tool_call.arguments, - name=tool_call.name, - output=output, + arguments=event.tool_call.arguments, + name=event.tool_call.name, + output=event.output, ) await self._emit_event(RealtimeModelItemUpdatedEvent(item=tool_item)) - if start_response: - await self.send_event({"type": "response.create"}) + if event.start_response: + await self._send_raw_message( + RealtimeModelSendRawMessage(message={"type": "response.create"}) + ) - async def interrupt(self) -> None: - """Interrupt the model.""" + async def _send_interrupt(self, event: RealtimeModelSendInterrupt) -> None: + """Send an interrupt to the model.""" if not self._current_item_id or not self._audio_start_time: return @@ -278,15 +308,17 @@ async def interrupt(self) -> None: elapsed_time_ms = (datetime.now() - self._audio_start_time).total_seconds() * 1000 if elapsed_time_ms > 0 and elapsed_time_ms < self._audio_length_ms: await self._emit_event(RealtimeModelAudioInterruptedEvent()) - await self.send_event( - { - "type": "conversation.item.truncate", - "other_data": { - "item_id": self._current_item_id, - "content_index": self._current_audio_content_index, - "audio_end_ms": elapsed_time_ms, - }, - } + await self._send_raw_message( + RealtimeModelSendRawMessage( + message={ + "type": "conversation.item.truncate", + "other_data": { + "item_id": self._current_item_id, + "content_index": self._current_audio_content_index, + "audio_end_ms": elapsed_time_ms, + }, + } + ) ) self._current_item_id = None @@ -376,7 +408,9 @@ async def close(self) -> None: async def _cancel_response(self) -> None: if self._ongoing_response: - await self.send_event({"type": "response.cancel"}) + await self._send_raw_message( + RealtimeModelSendRawMessage(message={"type": "response.cancel"}) + ) self._ongoing_response = False async def _handle_ws_event(self, event: dict[str, Any]): @@ -399,7 +433,7 @@ async def _handle_ws_event(self, event: dict[str, Any]): elif parsed.type == "response.audio.done": await self._emit_event(RealtimeModelAudioDoneEvent()) elif parsed.type == "input_audio_buffer.speech_started": - await self.interrupt() + await self._send_interrupt(RealtimeModelSendInterrupt()) elif parsed.type == "response.created": self._ongoing_response = True await self._emit_event(RealtimeModelTurnStartedEvent()) @@ -424,13 +458,15 @@ async def _handle_ws_event(self, event: dict[str, Any]): parsed.type == "conversation.item.input_audio_transcription.completed" or parsed.type == "conversation.item.truncated" ): - await self.send_event( - { - "type": "conversation.item.retrieve", - "other_data": { - "item_id": self._current_item_id, - }, - } + await self._send_raw_message( + RealtimeModelSendRawMessage( + message={ + "type": "conversation.item.retrieve", + "other_data": { + "item_id": self._current_item_id, + }, + } + ) ) if parsed.type == "conversation.item.input_audio_transcription.completed": await self._emit_event( diff --git a/src/agents/realtime/session.py b/src/agents/realtime/session.py index ce8b7d705..04ce09bea 100644 --- a/src/agents/realtime/session.py +++ b/src/agents/realtime/session.py @@ -36,6 +36,12 @@ RealtimeModelInputAudioTranscriptionCompletedEvent, RealtimeModelToolCallEvent, ) +from .model_inputs import ( + RealtimeModelSendAudio, + RealtimeModelSendInterrupt, + RealtimeModelSendToolOutput, + RealtimeModelSendUserInput, +) class RealtimeSession(RealtimeModelListener): @@ -148,15 +154,15 @@ async def close(self) -> None: async def send_message(self, message: RealtimeUserInput) -> None: """Send a message to the model.""" - await self._model.send_message(message) + await self._model.send_event(RealtimeModelSendUserInput(user_input=message)) async def send_audio(self, audio: bytes, *, commit: bool = False) -> None: """Send a raw audio chunk to the model.""" - await self._model.send_audio(audio, commit=commit) + await self._model.send_event(RealtimeModelSendAudio(audio=audio, commit=commit)) async def interrupt(self) -> None: """Interrupt the model.""" - await self._model.interrupt() + await self._model.send_event(RealtimeModelSendInterrupt()) async def on_event(self, event: RealtimeModelEvent) -> None: await self._put_event(RealtimeRawModelEvent(data=event, info=self._event_info)) @@ -171,7 +177,7 @@ async def on_event(self, event: RealtimeModelEvent) -> None: await self._put_event(RealtimeAudioInterrupted(info=self._event_info)) elif event.type == "audio_done": await self._put_event(RealtimeAudioEnd(info=self._event_info)) - elif event.type == "conversation.item.input_audio_transcription.completed": + elif event.type == "input_audio_transcription_completed": self._history = RealtimeSession._get_new_history(self._history, event) await self._put_event( RealtimeHistoryUpdated(info=self._event_info, history=self._history) @@ -263,7 +269,9 @@ async def _handle_tool_call(self, event: RealtimeModelToolCallEvent) -> None: tool_context = ToolContext.from_agent_context(self._context_wrapper, event.call_id) result = await func_tool.on_invoke_tool(tool_context, event.arguments) - await self._model.send_tool_output(event, str(result), True) + await self._model.send_event(RealtimeModelSendToolOutput( + tool_call=event, output=str(result), start_response=True + )) await self._put_event( RealtimeToolEnd( @@ -367,11 +375,13 @@ async def _run_output_guardrails(self, text: str) -> bool: ) # Interrupt the model - await self._model.interrupt() + await self._model.send_event(RealtimeModelSendInterrupt()) # Send guardrail triggered message guardrail_names = [result.guardrail.get_name() for result in triggered_results] - await self._model.send_message(f"guardrail triggered: {', '.join(guardrail_names)}") + await self._model.send_event(RealtimeModelSendUserInput( + user_input=f"guardrail triggered: {', '.join(guardrail_names)}" + )) return True diff --git a/src/agents/voice/models/openai_stt.py b/src/agents/voice/models/openai_stt.py index 1ae4ea147..b1f1b6da7 100644 --- a/src/agents/voice/models/openai_stt.py +++ b/src/agents/voice/models/openai_stt.py @@ -226,7 +226,7 @@ async def _handle_events(self) -> None: break event_type = event.get("type", "unknown") - if event_type == "conversation.item.input_audio_transcription.completed": + if event_type == "input_audio_transcription_completed": transcript = cast(str, event.get("transcript", "")) if len(transcript) > 0: self._end_turn(transcript) diff --git a/tests/realtime/test_session.py b/tests/realtime/test_session.py index f6bd60064..4cee9e537 100644 --- a/tests/realtime/test_session.py +++ b/tests/realtime/test_session.py @@ -56,6 +56,8 @@ def __init__(self): self.listeners = [] self.connect_called = False self.close_called = False + self.sent_events = [] + # Legacy tracking for tests that haven't been updated yet self.sent_messages = [] self.sent_audio = [] self.sent_tool_outputs = [] @@ -72,19 +74,24 @@ def remove_listener(self, listener): self.listeners.remove(listener) async def send_event(self, event): - pass - - async def send_message(self, message, other_event_data=None): - self.sent_messages.append(message) - - async def send_audio(self, audio, commit=False): - self.sent_audio.append((audio, commit)) - - async def send_tool_output(self, tool_call, output, start_response=True): - self.sent_tool_outputs.append((tool_call, output, start_response)) + from agents.realtime.model_inputs import ( + RealtimeModelSendAudio, + RealtimeModelSendInterrupt, + RealtimeModelSendToolOutput, + RealtimeModelSendUserInput, + ) - async def interrupt(self): - self.interrupts_called += 1 + self.sent_events.append(event) + + # Update legacy tracking for compatibility + if isinstance(event, RealtimeModelSendUserInput): + self.sent_messages.append(event.user_input) + elif isinstance(event, RealtimeModelSendAudio): + self.sent_audio.append((event.audio, event.commit)) + elif isinstance(event, RealtimeModelSendToolOutput): + self.sent_tool_outputs.append((event.tool_call, event.output, event.start_response)) + elif isinstance(event, RealtimeModelSendInterrupt): + self.interrupts_called += 1 async def close(self): self.close_called = True diff --git a/tests/realtime/test_tracing.py b/tests/realtime/test_tracing.py index 7fb2594a9..3548829dd 100644 --- a/tests/realtime/test_tracing.py +++ b/tests/realtime/test_tracing.py @@ -95,12 +95,13 @@ async def async_websocket(*args, **kwargs): "session": {"id": "session_456"}, } - with patch.object(model, "send_event") as mock_send_event: + with patch.object(model, "_send_raw_message") as mock_send_raw_message: await model._handle_ws_event(session_created_event) # Should send session.update with tracing config - mock_send_event.assert_called_once_with( - { + from agents.realtime.model_inputs import RealtimeModelSendRawMessage + expected_event = RealtimeModelSendRawMessage( + message={ "type": "session.update", "other_data": { "session": { @@ -112,6 +113,7 @@ async def async_websocket(*args, **kwargs): }, } ) + mock_send_raw_message.assert_called_once_with(expected_event) @pytest.mark.asyncio async def test_send_tracing_config_auto_mode(self, model, mock_websocket): @@ -137,13 +139,18 @@ async def async_websocket(*args, **kwargs): "session": {"id": "session_456"}, } - with patch.object(model, "send_event") as mock_send_event: + with patch.object(model, "_send_raw_message") as mock_send_raw_message: await model._handle_ws_event(session_created_event) # Should send session.update with "auto" - mock_send_event.assert_called_once_with( - {"type": "session.update", "other_data": {"session": {"tracing": "auto"}}} + from agents.realtime.model_inputs import RealtimeModelSendRawMessage + expected_event = RealtimeModelSendRawMessage( + message={ + "type": "session.update", + "other_data": {"session": {"tracing": "auto"}}, + } ) + mock_send_raw_message.assert_called_once_with(expected_event) @pytest.mark.asyncio async def test_tracing_config_none_skips_session_update(self, model, mock_websocket): @@ -196,22 +203,25 @@ async def async_websocket(*args, **kwargs): "session": {"id": "session_456"}, } - with patch.object(model, "send_event") as mock_send_event: + with patch.object(model, "_send_raw_message") as mock_send_raw_message: await model._handle_ws_event(session_created_event) # Should send session.update with complete tracing config including metadata - expected_call = { - "type": "session.update", - "other_data": { - "session": { - "tracing": { - "workflow_name": "complex_workflow", - "metadata": complex_metadata, + from agents.realtime.model_inputs import RealtimeModelSendRawMessage + expected_event = RealtimeModelSendRawMessage( + message={ + "type": "session.update", + "other_data": { + "session": { + "tracing": { + "workflow_name": "complex_workflow", + "metadata": complex_metadata, + } } - } - }, - } - mock_send_event.assert_called_once_with(expected_call) + }, + } + ) + mock_send_raw_message.assert_called_once_with(expected_event) @pytest.mark.asyncio async def test_tracing_disabled_prevents_tracing(self, mock_websocket): diff --git a/tests/voice/test_openai_stt.py b/tests/voice/test_openai_stt.py index 89b5cca70..ecc41f2e2 100644 --- a/tests/voice/test_openai_stt.py +++ b/tests/voice/test_openai_stt.py @@ -186,7 +186,7 @@ async def test_stream_audio_sends_correct_json(): @pytest.mark.asyncio async def test_transcription_event_puts_output_in_queue(): """ - Test that a 'conversation.item.input_audio_transcription.completed' event + Test that a 'input_audio_transcription_completed' event yields a transcript from transcribe_turns(). """ mock_ws = create_mock_websocket( @@ -196,7 +196,7 @@ async def test_transcription_event_puts_output_in_queue(): # Once configured, we mock a completed transcription event: json.dumps( { - "type": "conversation.item.input_audio_transcription.completed", + "type": "input_audio_transcription_completed", "transcript": "Hello world!", } ),