from __future__ import annotations from typing import Any, cast from unittest.mock import AsyncMock import pytest from openai.types.realtime.realtime_session_create_request import ( RealtimeSessionCreateRequest, ) from openai.types.realtime.session_update_event import SessionUpdateEvent from agents.handoffs import Handoff from agents.realtime.agent import RealtimeAgent from agents.realtime.config import RealtimeRunConfig, RealtimeSessionModelSettings from agents.realtime.handoffs import realtime_handoff from agents.realtime.model import RealtimeModelConfig from agents.realtime.openai_realtime import ( OpenAIRealtimeSIPModel, OpenAIRealtimeWebSocketModel, _build_model_settings_from_agent, _collect_enabled_handoffs, ) from agents.run_context import RunContextWrapper from agents.tool import FunctionTool, function_tool def _disabled_billing_realtime_handoff(*, is_enabled: Any = False) -> Handoff[Any, Any]: return realtime_handoff( RealtimeAgent(name="billing"), tool_name_override="transfer_to_billing", is_enabled=is_enabled, ) def _disabled_billing_realtime_tool(*, is_enabled: Any = False) -> FunctionTool: return function_tool( lambda: "ok", name_override="transfer_to_billing", is_enabled=is_enabled, ) @pytest.mark.asyncio async def test_collect_enabled_handoffs_filters_disabled() -> None: parent = RealtimeAgent(name="parent") disabled = realtime_handoff( RealtimeAgent(name="child_disabled"), is_enabled=lambda ctx, agent: False, ) parent.handoffs = [disabled, RealtimeAgent(name="child_enabled")] enabled = await _collect_enabled_handoffs(parent, RunContextWrapper(None)) assert len(enabled) == 1 assert isinstance(enabled[0], Handoff) assert enabled[0].agent_name == "child_enabled" @pytest.mark.asyncio async def test_build_model_settings_from_agent_merges_agent_fields(monkeypatch: pytest.MonkeyPatch): agent = RealtimeAgent(name="root", prompt={"id": "prompt-id"}) monkeypatch.setattr(agent, "get_system_prompt", AsyncMock(return_value="sys")) @function_tool def helper() -> str: """Helper tool for testing.""" return "ok" monkeypatch.setattr(agent, "get_all_tools", AsyncMock(return_value=[helper])) agent.handoffs = [RealtimeAgent(name="handoff-child")] base_settings: RealtimeSessionModelSettings = {"model_name": "gpt-realtime-2.1"} starting_settings: RealtimeSessionModelSettings = {"voice": "verse"} run_config: RealtimeRunConfig = {"tracing_disabled": True} merged = await _build_model_settings_from_agent( agent=agent, context_wrapper=RunContextWrapper(None), base_settings=base_settings, starting_settings=starting_settings, run_config=run_config, ) assert merged["prompt"] == {"id": "prompt-id"} assert merged["instructions"] == "sys" assert merged["tools"][0].name == helper.name assert merged["handoffs"][0].agent_name == "handoff-child" assert merged["voice"] == "verse" assert merged["model_name"] == "gpt-realtime-2.1" assert merged["tracing"] is None assert base_settings == {"model_name": "gpt-realtime-2.1"} @pytest.mark.asyncio async def test_build_model_settings_filters_disabled_starting_handoff_name_conflict(): tool = function_tool(lambda: "ok", name_override="transfer_to_billing") disabled_handoff = _disabled_billing_realtime_handoff() agent = RealtimeAgent(name="parent", tools=[tool]) merged = await _build_model_settings_from_agent( agent=agent, context_wrapper=RunContextWrapper(None), base_settings={}, starting_settings={"handoffs": [disabled_handoff]}, run_config=None, ) assert merged["tools"] == [tool] assert merged["handoffs"] == [] @pytest.mark.asyncio async def test_build_model_settings_filters_disabled_starting_tool_name_conflict(): disabled_tool = _disabled_billing_realtime_tool() handoff = _disabled_billing_realtime_handoff(is_enabled=True) agent = RealtimeAgent(name="parent", handoffs=[handoff]) merged = await _build_model_settings_from_agent( agent=agent, context_wrapper=RunContextWrapper(None), base_settings={}, starting_settings={"tools": [disabled_tool]}, run_config=None, ) assert merged["tools"] == [] assert merged["handoffs"] == [handoff] @pytest.mark.asyncio async def test_build_model_settings_evaluates_starting_tool_is_enabled_callable(): calls: list[tuple[RunContextWrapper[Any], RealtimeAgent[Any]]] = [] async def is_enabled(ctx: RunContextWrapper[Any], agent_arg: RealtimeAgent[Any]) -> bool: calls.append((ctx, agent_arg)) return False disabled_tool = _disabled_billing_realtime_tool(is_enabled=is_enabled) agent = RealtimeAgent(name="parent") context_wrapper = RunContextWrapper(None) merged = await _build_model_settings_from_agent( agent=agent, context_wrapper=context_wrapper, base_settings={}, starting_settings={"tools": [disabled_tool]}, run_config=None, ) assert merged["tools"] == [] assert calls == [(context_wrapper, agent)] @pytest.mark.asyncio async def test_build_model_settings_does_not_reevaluate_agent_handoff_without_override(): call_count = 0 async def is_enabled(ctx: RunContextWrapper[Any], agent_arg: RealtimeAgent[Any]) -> bool: nonlocal call_count call_count += 1 return call_count == 1 handoff = cast( Handoff[Any, Any], realtime_handoff(RealtimeAgent(name="billing"), is_enabled=is_enabled), ) agent = RealtimeAgent(name="parent", handoffs=[handoff]) merged = await _build_model_settings_from_agent( agent=agent, context_wrapper=RunContextWrapper(None), base_settings={}, starting_settings={"voice": "verse"}, run_config=None, ) assert merged["handoffs"] == [handoff] assert call_count == 1 @pytest.mark.asyncio async def test_sip_model_build_initial_session_payload(monkeypatch: pytest.MonkeyPatch): agent = RealtimeAgent(name="parent", prompt={"id": "prompt-99"}) child_agent = RealtimeAgent(name="child") agent.handoffs = [child_agent] @function_tool def ping() -> str: """Ping tool used for session payload building.""" return "pong" monkeypatch.setattr(agent, "get_system_prompt", AsyncMock(return_value="parent-system")) monkeypatch.setattr(agent, "get_all_tools", AsyncMock(return_value=[ping])) model_config: RealtimeModelConfig = { "initial_model_settings": { "model_name": "gpt-realtime-mini", "voice": "verse", } } run_config: RealtimeRunConfig = { "model_settings": {"output_modalities": ["text"]}, "tracing_disabled": True, } overrides: RealtimeSessionModelSettings = { "audio": {"input": {"format": {"type": "audio/pcmu"}}}, "output_audio_format": "g711_ulaw", } payload = await OpenAIRealtimeSIPModel.build_initial_session_payload( agent, context={"user": "abc"}, model_config=model_config, run_config=run_config, overrides=overrides, ) assert isinstance(payload, RealtimeSessionCreateRequest) assert payload.model == "gpt-realtime-mini" assert payload.output_modalities == ["text"] assert payload.audio is not None audio = payload.audio assert audio.input is not None assert audio.input.format is not None assert audio.input.format.type == "audio/pcmu" assert audio.output is not None assert audio.output.format is not None assert audio.output.format.type == "audio/pcmu" assert audio.output.voice == "verse" assert payload.instructions == "parent-system" assert payload.prompt is not None and payload.prompt.id == "prompt-99" tool_names: set[str] = set() for tool in payload.tools or []: name = getattr(tool, "name", None) if name: tool_names.add(name) assert ping.name in tool_names assert f"transfer_to_{child_agent.name}" in tool_names @pytest.mark.asyncio async def test_sip_initial_session_payload_filters_disabled_initial_model_settings_handoff(): tool = function_tool(lambda: "ok", name_override="transfer_to_billing") disabled_handoff = _disabled_billing_realtime_handoff() agent = RealtimeAgent(name="parent", tools=[tool]) payload = await OpenAIRealtimeSIPModel.build_initial_session_payload( agent, model_config={"initial_model_settings": {"handoffs": [disabled_handoff]}}, ) tool_names = [getattr(tool, "name", None) for tool in payload.tools or []] assert tool_names.count("transfer_to_billing") == 1 @pytest.mark.asyncio async def test_sip_initial_session_payload_filters_disabled_initial_model_settings_tool(): disabled_tool = _disabled_billing_realtime_tool() agent = RealtimeAgent( name="parent", handoffs=[_disabled_billing_realtime_handoff(is_enabled=True)], ) payload = await OpenAIRealtimeSIPModel.build_initial_session_payload( agent, model_config={"initial_model_settings": {"tools": [disabled_tool]}}, ) tool_names = [getattr(tool, "name", None) for tool in payload.tools or []] assert tool_names == ["transfer_to_billing"] @pytest.mark.asyncio async def test_sip_initial_session_payload_filters_disabled_override_handoff(): tool = function_tool(lambda: "ok", name_override="transfer_to_billing") disabled_handoff = _disabled_billing_realtime_handoff() agent = RealtimeAgent(name="parent", tools=[tool]) payload = await OpenAIRealtimeSIPModel.build_initial_session_payload( agent, overrides={"handoffs": [disabled_handoff]}, ) tool_names = [getattr(tool, "name", None) for tool in payload.tools or []] assert tool_names.count("transfer_to_billing") == 1 @pytest.mark.asyncio async def test_sip_initial_session_payload_filters_disabled_override_tool(): disabled_tool = _disabled_billing_realtime_tool() agent = RealtimeAgent( name="parent", handoffs=[_disabled_billing_realtime_handoff(is_enabled=True)], ) payload = await OpenAIRealtimeSIPModel.build_initial_session_payload( agent, overrides={"tools": [disabled_tool]}, ) tool_names = [getattr(tool, "name", None) for tool in payload.tools or []] assert tool_names == ["transfer_to_billing"] @pytest.mark.asyncio async def test_sip_initial_session_payload_does_not_reevaluate_agent_handoff_without_override(): call_count = 0 async def is_enabled(ctx: RunContextWrapper[Any], agent_arg: RealtimeAgent[Any]) -> bool: nonlocal call_count call_count += 1 return call_count == 1 handoff = cast( Handoff[Any, Any], realtime_handoff(RealtimeAgent(name="billing"), is_enabled=is_enabled), ) agent = RealtimeAgent(name="parent", handoffs=[handoff]) payload = await OpenAIRealtimeSIPModel.build_initial_session_payload( agent, overrides={"voice": "verse"}, ) tool_names = [getattr(tool, "name", None) for tool in payload.tools or []] assert "transfer_to_billing" in tool_names assert call_count == 1 def test_call_id_session_update_omits_null_audio_formats() -> None: model = OpenAIRealtimeWebSocketModel() model._call_id = "call_123" session_config = model._get_session_config({}) payload = SessionUpdateEvent(type="session.update", session=session_config).model_dump( exclude_unset=True ) audio = payload["session"]["audio"] assert "format" not in audio["input"] assert "format" not in audio["output"] def test_call_id_session_update_includes_explicit_audio_formats() -> None: model = OpenAIRealtimeWebSocketModel() model._call_id = "call_123" session_config = model._get_session_config( { "input_audio_format": "g711_ulaw", "output_audio_format": "g711_ulaw", } ) payload = SessionUpdateEvent(type="session.update", session=session_config).model_dump( exclude_unset=True ) audio = payload["session"]["audio"] assert audio["input"]["format"]["type"] == "audio/pcmu" assert audio["output"]["format"]["type"] == "audio/pcmu"