"""Tests for realtime handoff functionality.""" import asyncio import inspect from collections.abc import Awaitable, Coroutine from typing import Any, cast from unittest.mock import Mock import pytest from pydantic import BaseModel from agents import Agent from agents.exceptions import ModelBehaviorError, UserError from agents.realtime import RealtimeAgent, realtime_handoff from agents.run_context import RunContextWrapper def test_realtime_handoff_creation(): """Test basic realtime handoff creation.""" realtime_agent = RealtimeAgent(name="test_agent") handoff_obj = realtime_handoff(realtime_agent) assert handoff_obj.agent_name == "test_agent" assert handoff_obj.tool_name == "transfer_to_test_agent" assert handoff_obj.input_filter is None # Should not support input filters assert handoff_obj.is_enabled is True def test_realtime_handoff_with_custom_params(): """Test realtime handoff with custom parameters.""" realtime_agent = RealtimeAgent( name="helper_agent", handoff_description="Helps with general tasks", ) handoff_obj = realtime_handoff( realtime_agent, tool_name_override="custom_handoff", tool_description_override="Custom handoff description", is_enabled=False, ) assert handoff_obj.agent_name == "helper_agent" assert handoff_obj.tool_name == "custom_handoff" assert handoff_obj.tool_description == "Custom handoff description" assert handoff_obj.is_enabled is False @pytest.mark.asyncio async def test_realtime_handoff_execution(): """Test that realtime handoff returns the correct agent.""" realtime_agent = RealtimeAgent(name="target_agent") handoff_obj = realtime_handoff(realtime_agent) # Mock context mock_context = Mock() # Execute handoff result = await handoff_obj.on_invoke_handoff(mock_context, "") assert result is realtime_agent assert isinstance(result, RealtimeAgent) def test_realtime_handoff_with_on_handoff_callback(): """Test realtime handoff with custom on_handoff callback.""" realtime_agent = RealtimeAgent(name="callback_agent") callback_called = [] def on_handoff_callback(ctx): callback_called.append(True) handoff_obj = realtime_handoff( realtime_agent, on_handoff=on_handoff_callback, ) asyncio.run( cast( Coroutine[Any, Any, RealtimeAgent[Any]], handoff_obj.on_invoke_handoff(RunContextWrapper(None), ""), ) ) assert callback_called == [True] assert handoff_obj.agent_name == "callback_agent" def test_regular_agent_handoff_still_works(): """Test that regular Agent handoffs still work with the new generic types.""" from agents import handoff regular_agent = Agent(name="regular_agent") handoff_obj = handoff(regular_agent) assert handoff_obj.agent_name == "regular_agent" assert handoff_obj.tool_name == "transfer_to_regular_agent" # Regular agent handoffs should support input filters assert hasattr(handoff_obj, "input_filter") def test_type_annotations_work(): """Test that type annotations work correctly.""" from agents.handoffs import Handoff from agents.realtime.handoffs import realtime_handoff realtime_agent = RealtimeAgent(name="typed_agent") handoff_obj = realtime_handoff(realtime_agent) # This should be typed as Handoff[Any, RealtimeAgent[Any]] assert isinstance(handoff_obj, Handoff) def test_realtime_handoff_invalid_param_counts_raise(): rt = RealtimeAgent(name="x") # on_handoff with input_type but wrong param count def bad2(a): # only one parameter return None assert bad2(None) is None with pytest.raises(UserError): realtime_handoff(rt, on_handoff=bad2, input_type=int) # type: ignore[arg-type] # on_handoff without input but wrong param count def bad1(a, b): # two parameters return None assert bad1(None, None) is None with pytest.raises(UserError): realtime_handoff(rt, on_handoff=bad1) # type: ignore[arg-type] def test_realtime_handoff_input_type_requires_on_handoff(): """input_type without on_handoff must raise UserError, not silently produce a broken handoff.""" rt = RealtimeAgent(name="x") with pytest.raises(UserError): realtime_handoff(rt, input_type=int) # type: ignore[call-overload] def test_realtime_handoff_non_callable_on_handoff_raises_error(): """Providing a non-callable on_handoff with input_type should raise UserError.""" rt = RealtimeAgent(name="x") with pytest.raises(UserError, match="on_handoff must be callable"): realtime_handoff(rt, on_handoff="not_a_function", input_type=int) # type: ignore[call-overload] @pytest.mark.asyncio async def test_realtime_handoff_missing_input_json_raises_model_error(): rt = RealtimeAgent(name="x") async def with_input(ctx: RunContextWrapper[Any], data: int): # simple non-object type return None h = realtime_handoff(rt, on_handoff=with_input, input_type=int) with pytest.raises(ModelBehaviorError): await h.on_invoke_handoff(RunContextWrapper(None), "null") await with_input(RunContextWrapper(None), 1) @pytest.mark.asyncio async def test_realtime_handoff_is_enabled_async(monkeypatch): rt = RealtimeAgent(name="x") async def is_enabled(ctx, agent): return True h = realtime_handoff(rt, is_enabled=is_enabled) assert callable(h.is_enabled) result = h.is_enabled(RunContextWrapper(None), rt) assert isinstance(result, Awaitable) assert await result @pytest.mark.asyncio async def test_realtime_handoff_rejects_none_input() -> None: rt = RealtimeAgent(name="x") async def with_input(ctx: RunContextWrapper[Any], data: int) -> None: return None handoff_obj = realtime_handoff(rt, on_handoff=with_input, input_type=int) with pytest.raises(ModelBehaviorError): await handoff_obj.on_invoke_handoff(RunContextWrapper(None), cast(str, None)) await with_input(RunContextWrapper(None), 2) @pytest.mark.asyncio async def test_realtime_handoff_sync_is_enabled_callable() -> None: rt = RealtimeAgent(name="x") calls: list[bool] = [] def is_enabled(ctx: RunContextWrapper[Any], agent: RealtimeAgent[Any]) -> bool: calls.append(True) assert agent is rt return False handoff_obj = realtime_handoff(rt, is_enabled=is_enabled) assert callable(handoff_obj.is_enabled) enabled_result = handoff_obj.is_enabled(RunContextWrapper(None), rt) if inspect.isawaitable(enabled_result): assert await enabled_result is False else: assert enabled_result is False assert calls, "is_enabled callback should be invoked" def test_realtime_handoff_sync_on_handoff_executes() -> None: rt = RealtimeAgent(name="sync") called: list[int] = [] def on_handoff(ctx: RunContextWrapper[Any], value: int) -> None: called.append(value) handoff_obj = realtime_handoff(rt, on_handoff=on_handoff, input_type=int) result: RealtimeAgent[Any] = asyncio.run( cast( Coroutine[Any, Any, RealtimeAgent[Any]], handoff_obj.on_invoke_handoff(RunContextWrapper(None), "5"), ) ) assert result is rt assert called == [5] def test_realtime_handoff_on_handoff_without_input_runs() -> None: rt = RealtimeAgent(name="no_input") called: list[bool] = [] def on_handoff(ctx: RunContextWrapper[Any]) -> None: called.append(True) handoff_obj = realtime_handoff(rt, on_handoff=on_handoff) result: RealtimeAgent[Any] = asyncio.run( cast( Coroutine[Any, Any, RealtimeAgent[Any]], handoff_obj.on_invoke_handoff(RunContextWrapper(None), ""), ) ) assert result is rt assert called == [True] @pytest.mark.asyncio async def test_realtime_handoff_async_on_handoff_without_input_runs() -> None: rt = RealtimeAgent(name="async_no_input") called: list[bool] = [] async def on_handoff(ctx: RunContextWrapper[Any]) -> None: called.append(True) handoff_obj = realtime_handoff(rt, on_handoff=on_handoff) result = await handoff_obj.on_invoke_handoff(RunContextWrapper(None), "") assert result is rt assert called == [True] class StrictInput(BaseModel): name: str age: int @pytest.mark.asyncio async def test_realtime_handoff_strict_json_rejects_type_coercion(): """With strict_json_schema=True (always on for realtime handoffs), string input for an int field must raise ModelBehaviorError instead of being silently coerced.""" rt = RealtimeAgent(name="strict_test") async def _on_handoff(ctx: RunContextWrapper[Any], data: StrictInput) -> None: pass # pragma: no cover handoff_obj = realtime_handoff(rt, on_handoff=_on_handoff, input_type=StrictInput) # age is a string "25" — strict mode should reject this malformed_json = '{"name": "Alice", "age": "25"}' with pytest.raises(ModelBehaviorError, match="Invalid JSON"): await handoff_obj.on_invoke_handoff(RunContextWrapper(None), malformed_json) # Correctly typed input should still be accepted valid_json = '{"name": "Alice", "age": 25}' result = await handoff_obj.on_invoke_handoff(RunContextWrapper(None), valid_json) assert result is rt