From 3178318f60868e501210c3ac7fe782c06448dbb9 Mon Sep 17 00:00:00 2001 From: Patrick Gray Date: Sun, 30 Nov 2025 15:29:47 -0500 Subject: [PATCH 1/2] fix formatting on docstrings --- src/strands/experimental/bidi/agent/agent.py | 2 ++ src/strands/experimental/bidi/io/audio.py | 2 ++ src/strands/experimental/bidi/io/text.py | 1 + src/strands/experimental/bidi/models/bidi_model.py | 4 ++++ src/strands/experimental/bidi/models/gemini_live.py | 2 ++ src/strands/experimental/bidi/models/novasonic.py | 1 + src/strands/experimental/bidi/models/openai.py | 4 ++-- src/strands/experimental/bidi/types/events.py | 6 +++++- src/strands/experimental/hooks/__init__.py | 5 +---- src/strands/experimental/hooks/events.py | 11 ++++------- tests/strands/experimental/bidi/models/test_openai.py | 8 ++++---- 11 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/strands/experimental/bidi/agent/agent.py b/src/strands/experimental/bidi/agent/agent.py index 74b65ba10..2bfbdb3fa 100644 --- a/src/strands/experimental/bidi/agent/agent.py +++ b/src/strands/experimental/bidi/agent/agent.py @@ -6,6 +6,7 @@ continuous responses including audio output. Key capabilities: + - Persistent conversation connections with concurrent processing - Real-time audio input/output streaming - Automatic interruption detection and tool execution @@ -233,6 +234,7 @@ async def send(self, input_data: BidiAgentInput | dict[str, Any]) -> None: Args: input_data: Can be: + - str: Text message from user - BidiInputEvent: TypedEvent - dict: Event dictionary (will be reconstructed to TypedEvent) diff --git a/src/strands/experimental/bidi/io/audio.py b/src/strands/experimental/bidi/io/audio.py index b5404b749..5eff829e9 100644 --- a/src/strands/experimental/bidi/io/audio.py +++ b/src/strands/experimental/bidi/io/audio.py @@ -73,6 +73,7 @@ def get(self, byte_count: int | None = None) -> bytes: Args: byte_count: Number of bytes to get from buffer. + - If the number of bytes specified is not available, the return is padded with silence. - If the number of bytes is not specified, get the first chunk put in the buffer. @@ -274,6 +275,7 @@ def __init__(self, **config: Any) -> None: Args: **config: Optional device configuration: + - input_buffer_size (int): Maximum input buffer size (default: None) - input_device_index (int): Specific input device (default: None = system default) - input_frames_per_buffer (int): Input buffer size (default: 512) diff --git a/src/strands/experimental/bidi/io/text.py b/src/strands/experimental/bidi/io/text.py index 1fe906de0..f575c5606 100644 --- a/src/strands/experimental/bidi/io/text.py +++ b/src/strands/experimental/bidi/io/text.py @@ -73,6 +73,7 @@ def __init__(self, **config: Any) -> None: Args: **config: Optional I/O configurations. + - input_prompt (str): Input prompt to display on screen (default: blank) """ self._config = config diff --git a/src/strands/experimental/bidi/models/bidi_model.py b/src/strands/experimental/bidi/models/bidi_model.py index 0d0da63d2..f5e34aa50 100644 --- a/src/strands/experimental/bidi/models/bidi_model.py +++ b/src/strands/experimental/bidi/models/bidi_model.py @@ -6,6 +6,7 @@ text, and tool interactions. Features: + - Persistent connection management with connect/close lifecycle - Real-time bidirectional communication (send and receive simultaneously) - Provider-agnostic event normalization @@ -96,16 +97,19 @@ async def send( Args: content: The content to send. Must be one of: + - BidiTextInputEvent: Text message from the user - BidiAudioInputEvent: Audio data for speech input - BidiImageInputEvent: Image data for visual understanding - ToolResultEvent: Result from a tool execution Example: + ``` await model.send(BidiTextInputEvent(text="Hello", role="user")) await model.send(BidiAudioInputEvent(audio=bytes, format="pcm", sample_rate=16000, channels=1)) await model.send(BidiImageInputEvent(image=bytes, mime_type="image/jpeg", encoding="raw")) await model.send(ToolResultEvent(tool_result)) + ``` """ ... diff --git a/src/strands/experimental/bidi/models/gemini_live.py b/src/strands/experimental/bidi/models/gemini_live.py index 1f2b2d5cd..b8daff291 100644 --- a/src/strands/experimental/bidi/models/gemini_live.py +++ b/src/strands/experimental/bidi/models/gemini_live.py @@ -4,6 +4,7 @@ official Google GenAI SDK for simplified and robust WebSocket communication. Key improvements over custom WebSocket implementation: + - Uses official google-genai SDK with native Live API support - Simplified session management with client.aio.live.connect() - Built-in tool integration and event handling @@ -221,6 +222,7 @@ def _convert_gemini_live_event(self, message: LiveServerMessage) -> list[BidiOut """Convert Gemini Live API events to provider-agnostic format. Handles different types of content: + - inputTranscription: User's speech transcribed to text - outputTranscription: Model's audio transcribed to text - modelTurn text: Text response from the model diff --git a/src/strands/experimental/bidi/models/novasonic.py b/src/strands/experimental/bidi/models/novasonic.py index 713afe028..968c42358 100644 --- a/src/strands/experimental/bidi/models/novasonic.py +++ b/src/strands/experimental/bidi/models/novasonic.py @@ -5,6 +5,7 @@ InvokeModelWithBidirectionalStream protocol. Nova Sonic specifics: + - Hierarchical event sequences: connectionStart → promptStart → content streaming - Base64-encoded audio format with hex encoding - Tool execution with content containers and identifier tracking diff --git a/src/strands/experimental/bidi/models/openai.py b/src/strands/experimental/bidi/models/openai.py index bfe3ad533..af38ef706 100644 --- a/src/strands/experimental/bidi/models/openai.py +++ b/src/strands/experimental/bidi/models/openai.py @@ -48,8 +48,8 @@ """Max timeout before closing connection. OpenAI documents a 60 minute limit on realtime sessions -(https://platform.openai.com/docs/guides/realtime-conversations#session-lifecycle-events). However, OpenAI does not -emit any warnings when approaching the limit. As a workaround, we configure a max timeout client side to gracefully +([docs](https://platform.openai.com/docs/guides/realtime-conversations#session-lifecycle-events)). However, OpenAI does +not emit any warnings when approaching the limit. As a workaround, we configure a max timeout client side to gracefully handle the connection closure. We set the max to 50 minutes to provide enough buffer before hitting the real limit. """ OPENAI_REALTIME_URL = "wss://api.openai.com/v1/realtime" diff --git a/src/strands/experimental/bidi/types/events.py b/src/strands/experimental/bidi/types/events.py index 7ea2b6345..572ab56db 100644 --- a/src/strands/experimental/bidi/types/events.py +++ b/src/strands/experimental/bidi/types/events.py @@ -4,6 +4,7 @@ capabilities with real-time audio and persistent connection support. Key features: + - Audio input/output events with standardized formats - Interruption detection and handling - Connection lifecycle management @@ -12,6 +13,7 @@ - JSON-serializable events (audio/images stored as base64 strings) Audio format normalization: + - Supports PCM, WAV, Opus, and MP3 formats - Standardizes sample rates (16kHz, 24kHz, 48kHz) - Normalizes channel configurations (mono/stereo) @@ -29,6 +31,7 @@ AudioChannel = Literal[1, 2] """Number of audio channels. + - Mono: 1 - Stereo: 2 """ @@ -362,7 +365,6 @@ class BidiInterruptionEvent(TypedEvent): Parameters: reason: Why the interruption occurred. - response_id: ID of the response that was interrupted (may be None). """ def __init__(self, reason: Literal["user_speech", "error"]): @@ -592,6 +594,7 @@ def details(self) -> dict[str, Any] | None: # BidiInputEvent in send() methods for sending tool results back to the model. BidiInputEvent = BidiTextInputEvent | BidiAudioInputEvent | BidiImageInputEvent +"""Union of different bidi input event types.""" BidiOutputEvent = ( BidiConnectionStartEvent @@ -606,3 +609,4 @@ def details(self) -> dict[str, Any] | None: | BidiErrorEvent | ToolUseStreamEvent ) +"""Union of different bidi output event types.""" diff --git a/src/strands/experimental/hooks/__init__.py b/src/strands/experimental/hooks/__init__.py index 7c3c2b269..c76b57ea4 100644 --- a/src/strands/experimental/hooks/__init__.py +++ b/src/strands/experimental/hooks/__init__.py @@ -1,7 +1,4 @@ -"""Experimental hook functionality that has not yet reached stability. - -BidiAgent hooks are also available here to avoid circular imports. -""" +"""Experimental hook functionality that has not yet reached stability.""" from .events import ( AfterModelInvocationEvent, diff --git a/src/strands/experimental/hooks/events.py b/src/strands/experimental/hooks/events.py index f486f5ec4..8a8d80629 100644 --- a/src/strands/experimental/hooks/events.py +++ b/src/strands/experimental/hooks/events.py @@ -1,8 +1,6 @@ -"""Experimental hook events emitted as part of invoking Agents. +"""Experimental hook events emitted as part of invoking Agents and BidiAgents. -This module defines the events that are emitted as Agents run through the lifecycle of a request. - -BidiAgent hook events are also defined here to avoid circular imports. +This module defines the events that are emitted as Agents and BidiAgents run through the lifecycle of a request. """ import warnings @@ -19,8 +17,8 @@ from ..bidi.models import BidiModelTimeoutError warnings.warn( - "These events have been moved to production with updated names. Use BeforeModelCallEvent, " - "AfterModelCallEvent, BeforeToolCallEvent, and AfterToolCallEvent from strands.hooks instead.", + "BeforeModelCallEvent, AfterModelCallEvent, BeforeToolCallEvent, and AfterToolCallEvent are no longer experimental." + "Import from strands.hooks instead.", DeprecationWarning, stacklevel=2, ) @@ -32,7 +30,6 @@ # BidiAgent Hook Events -# These are defined here to avoid circular imports with the bidi package @dataclass diff --git a/tests/strands/experimental/bidi/models/test_openai.py b/tests/strands/experimental/bidi/models/test_openai.py index 85a1cc097..04381810e 100644 --- a/tests/strands/experimental/bidi/models/test_openai.py +++ b/tests/strands/experimental/bidi/models/test_openai.py @@ -816,7 +816,7 @@ async def test_tool_result_single_text_content(mock_websockets_connect, api_key) async def test_tool_result_single_json_content(mock_websockets_connect, api_key): """Test tool result with single JSON content block.""" _, mock_ws = mock_websockets_connect - model = BidiOpenAIRealtimeModel(api_key=api_key) + model = BidiOpenAIRealtimeModel(client_config={"api_key": api_key}) await model.start() tool_result: ToolResult = { @@ -846,7 +846,7 @@ async def test_tool_result_single_json_content(mock_websockets_connect, api_key) async def test_tool_result_multiple_content_blocks(mock_websockets_connect, api_key): """Test tool result with multiple content blocks (text and json).""" _, mock_ws = mock_websockets_connect - model = BidiOpenAIRealtimeModel(api_key=api_key) + model = BidiOpenAIRealtimeModel(client_config={"api_key": api_key}) await model.start() tool_result: ToolResult = { @@ -884,7 +884,7 @@ async def test_tool_result_multiple_content_blocks(mock_websockets_connect, api_ async def test_tool_result_image_content_raises_error(mock_websockets_connect, api_key): """Test that tool result with image content raises ValueError.""" _, mock_ws = mock_websockets_connect - model = BidiOpenAIRealtimeModel(api_key=api_key) + model = BidiOpenAIRealtimeModel(client_config={"api_key": api_key}) await model.start() tool_result: ToolResult = { @@ -903,7 +903,7 @@ async def test_tool_result_image_content_raises_error(mock_websockets_connect, a async def test_tool_result_document_content_raises_error(mock_websockets_connect, api_key): """Test that tool result with document content raises ValueError.""" _, mock_ws = mock_websockets_connect - model = BidiOpenAIRealtimeModel(api_key=api_key) + model = BidiOpenAIRealtimeModel(client_config={"api_key": api_key}) await model.start() tool_result: ToolResult = { From d707d5c077b57dfb8df67d462be827b0a96ac0d2 Mon Sep 17 00:00:00 2001 From: Patrick Gray Date: Sun, 30 Nov 2025 16:11:06 -0500 Subject: [PATCH 2/2] adjust unit test --- tests/strands/experimental/hooks/test_hook_aliases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/strands/experimental/hooks/test_hook_aliases.py b/tests/strands/experimental/hooks/test_hook_aliases.py index 6744aa00c..f4899f2ab 100644 --- a/tests/strands/experimental/hooks/test_hook_aliases.py +++ b/tests/strands/experimental/hooks/test_hook_aliases.py @@ -123,7 +123,7 @@ def test_deprecation_warning_on_import(captured_warnings): assert len(captured_warnings) == 1 assert issubclass(captured_warnings[0].category, DeprecationWarning) - assert "moved to production with updated names" in str(captured_warnings[0].message) + assert "are no longer experimental" in str(captured_warnings[0].message) def test_deprecation_warning_on_import_only_for_experimental(captured_warnings):