From 8dbc128eae1d5e9abe409d31a04418349ac417b2 Mon Sep 17 00:00:00 2001 From: mehtarac Date: Tue, 29 Sep 2026 16:41:47 -0400 Subject: [PATCH] feat(bidi): graduate bidirectional streaming API (#4707) Co-authored-by: Patrick Gray Co-authored-by: Murat Kaan Meral --- site/SITE-ARCHITECTURE.md | 2 +- site/scripts/api-generation-python.py | 11 +- .../sdk/bidirectional-streaming/agent.mdx | 25 +- .../sdk/bidirectional-streaming/barge-in.mdx | 25 +- .../sdk/bidirectional-streaming/events.mdx | 43 ++- .../sdk/bidirectional-streaming/hooks.mdx | 37 ++- .../sdk/bidirectional-streaming/index.mdx | 9 +- .../sdk/bidirectional-streaming/io.mdx | 39 ++- .../models/bedrock.mdx | 17 +- .../bidirectional-streaming/models/google.mdx | 17 +- .../bidirectional-streaming/models/openai.mdx | 15 +- .../bidirectional-streaming/observability.mdx | 35 +-- .../bidirectional-streaming/quickstart.mdx | 59 ++-- site/src/dynamic-sidebar.ts | 2 +- site/src/util/api-counterparts.ts | 7 +- site/src/util/api-link-converter.ts | 15 +- site/src/util/redirect.ts | 7 + site/test/api-link-converter.test.ts | 22 +- site/test/update-docs.test.ts | 2 +- strands-py/README.md | 8 +- strands-py/src/strands/_middleware/stages.py | 2 +- strands-py/src/strands/bidi/__init__.py | 6 + .../bidi/_async/__init__.py | 0 .../bidi/_async/_task_group.py | 0 .../bidi/_async/_task_pool.py | 0 .../bidi/_audio/__init__.py | 0 .../{experimental => }/bidi/_audio/buffer.py | 0 .../bidi/_audio/processor.py | 0 .../{experimental => }/bidi/_telemetry.py | 6 +- strands-py/src/strands/bidi/agent/__init__.py | 5 + .../{experimental => }/bidi/agent/_blocks.py | 2 +- .../bidi/agent/_reconnect_timer.py | 0 .../{experimental => }/bidi/agent/agent.py | 46 +-- .../{experimental => }/bidi/agent/loop.py | 8 +- strands-py/src/strands/bidi/hooks/__init__.py | 17 + .../{experimental => }/bidi/hooks/events.py | 2 +- strands-py/src/strands/bidi/io/__init__.py | 24 ++ .../{experimental => }/bidi/io/audio.py | 2 +- .../{experimental => }/bidi/io/configs.py | 0 .../bidi/io/console/__init__.py | 0 .../bidi/io/console/_display.py | 0 .../{experimental => }/bidi/io/console/_io.py | 2 +- .../bidi/io/console/_keyboard.py | 0 .../src/strands/bidi/models/__init__.py | 57 ++++ .../{experimental => }/bidi/models/bedrock.py | 6 +- .../{experimental => }/bidi/models/configs.py | 2 +- .../{experimental => }/bidi/models/google.py | 8 +- .../{experimental => }/bidi/models/model.py | 8 +- .../{experimental => }/bidi/models/openai.py | 6 +- strands-py/src/strands/bidi/types/__init__.py | 90 ++++++ .../{experimental => }/bidi/types/agent.py | 0 .../{experimental => }/bidi/types/content.py | 6 +- .../{experimental => }/bidi/types/events.py | 4 +- .../{experimental => }/bidi/types/io.py | 0 .../{experimental => }/bidi/types/media.py | 2 +- .../src/strands/experimental/bidi/__init__.py | 59 +++- .../experimental/bidi/agent/__init__.py | 12 +- .../experimental/bidi/hooks/__init__.py | 12 +- .../strands/experimental/bidi/io/__init__.py | 20 +- .../experimental/bidi/models/__init__.py | 35 +-- .../experimental/bidi/types/__init__.py | 37 ++- .../session/snapshot_session_manager.py | 4 +- strands-py/src/strands/tools/_caller.py | 2 +- .../src/strands/tools/executors/_executor.py | 2 +- .../tests/strands/agent/test_agent_storage.py | 4 +- strands-py/tests/strands/bidi/__init__.py | 1 + .../bidi/_async/__init__.py | 0 .../bidi/_async/test__init__.py | 2 +- .../bidi/_async/test_task_group.py | 2 +- .../bidi/_async/test_task_pool.py | 2 +- .../bidi/_audio/test_buffer.py | 2 +- .../bidi/_audio/test_processor.py | 2 +- .../{experimental => }/bidi/agent/__init__.py | 0 .../bidi/agent/test_agent.py | 14 +- .../bidi/agent/test_loop.py | 20 +- .../bidi/agent/test_loop_telemetry.py | 10 +- .../bidi/agent/test_reconnect_timer.py | 2 +- .../bidi/agent/test_snapshot.py | 4 +- .../{experimental => }/bidi/conftest.py | 4 +- .../{experimental => }/bidi/hooks/__init__.py | 0 .../bidi/hooks/test_events.py | 6 +- .../{experimental => }/bidi/io/__init__.py | 0 .../{experimental => }/bidi/io/conftest.py | 0 .../bidi/io/console/test_io.py | 4 +- .../bidi/io/console/test_keyboard.py | 2 +- .../{experimental => }/bidi/io/test_audio.py | 22 +- .../bidi/models/__init__.py | 0 .../bidi/models/test_bedrock.py | 20 +- .../bidi/models/test_configs.py | 6 +- .../bidi/models/test_google.py | 16 +- .../bidi/models/test_model.py | 4 +- .../bidi/models/test_openai.py | 12 +- .../{experimental => }/bidi/types/__init__.py | 0 .../bidi/types/test_events.py | 4 +- .../bidi/test_deprecated_alias.py | 297 ++++++++++++++++++ .../test_repository_session_manager.py | 2 +- .../session/test_snapshot_session_manager.py | 4 +- strands-py/tests/strands/tools/test_caller.py | 4 +- .../tests/strands/types/test_session.py | 2 +- strands-py/tests_integ/bidi/context.py | 2 +- strands-py/tests_integ/bidi/hook_utils.py | 2 +- .../tests_integ/bidi/test_bidi_hooks.py | 4 +- .../bidi/test_bidirectional_agent.py | 12 +- .../tests_integ/bidi/tools/test_direct.py | 2 +- .../bidi/test_deprecated_alias.py | 24 ++ strands-py/tests_typing/bidi/test_model.py | 2 +- strands-py/tests_typing/test_local_agent.py | 2 +- 107 files changed, 998 insertions(+), 417 deletions(-) create mode 100644 strands-py/src/strands/bidi/__init__.py rename strands-py/src/strands/{experimental => }/bidi/_async/__init__.py (100%) rename strands-py/src/strands/{experimental => }/bidi/_async/_task_group.py (100%) rename strands-py/src/strands/{experimental => }/bidi/_async/_task_pool.py (100%) rename strands-py/src/strands/{experimental => }/bidi/_audio/__init__.py (100%) rename strands-py/src/strands/{experimental => }/bidi/_audio/buffer.py (100%) rename strands-py/src/strands/{experimental => }/bidi/_audio/processor.py (100%) rename strands-py/src/strands/{experimental => }/bidi/_telemetry.py (97%) create mode 100644 strands-py/src/strands/bidi/agent/__init__.py rename strands-py/src/strands/{experimental => }/bidi/agent/_blocks.py (98%) rename strands-py/src/strands/{experimental => }/bidi/agent/_reconnect_timer.py (100%) rename strands-py/src/strands/{experimental => }/bidi/agent/agent.py (96%) rename strands-py/src/strands/{experimental => }/bidi/agent/loop.py (99%) create mode 100644 strands-py/src/strands/bidi/hooks/__init__.py rename strands-py/src/strands/{experimental => }/bidi/hooks/events.py (98%) create mode 100644 strands-py/src/strands/bidi/io/__init__.py rename strands-py/src/strands/{experimental => }/bidi/io/audio.py (99%) rename strands-py/src/strands/{experimental => }/bidi/io/configs.py (100%) rename strands-py/src/strands/{experimental => }/bidi/io/console/__init__.py (100%) rename strands-py/src/strands/{experimental => }/bidi/io/console/_display.py (100%) rename strands-py/src/strands/{experimental => }/bidi/io/console/_io.py (99%) rename strands-py/src/strands/{experimental => }/bidi/io/console/_keyboard.py (100%) create mode 100644 strands-py/src/strands/bidi/models/__init__.py rename strands-py/src/strands/{experimental => }/bidi/models/bedrock.py (99%) rename strands-py/src/strands/{experimental => }/bidi/models/configs.py (98%) rename strands-py/src/strands/{experimental => }/bidi/models/google.py (99%) rename strands-py/src/strands/{experimental => }/bidi/models/model.py (97%) rename strands-py/src/strands/{experimental => }/bidi/models/openai.py (99%) create mode 100644 strands-py/src/strands/bidi/types/__init__.py rename strands-py/src/strands/{experimental => }/bidi/types/agent.py (100%) rename strands-py/src/strands/{experimental => }/bidi/types/content.py (91%) rename strands-py/src/strands/{experimental => }/bidi/types/events.py (99%) rename strands-py/src/strands/{experimental => }/bidi/types/io.py (100%) rename strands-py/src/strands/{experimental => }/bidi/types/media.py (91%) create mode 100644 strands-py/tests/strands/bidi/__init__.py rename strands-py/tests/strands/{experimental => }/bidi/_async/__init__.py (100%) rename strands-py/tests/strands/{experimental => }/bidi/_async/test__init__.py (94%) rename strands-py/tests/strands/{experimental => }/bidi/_async/test_task_group.py (96%) rename strands-py/tests/strands/{experimental => }/bidi/_async/test_task_pool.py (93%) rename strands-py/tests/strands/{experimental => }/bidi/_audio/test_buffer.py (95%) rename strands-py/tests/strands/{experimental => }/bidi/_audio/test_processor.py (99%) rename strands-py/tests/strands/{experimental => }/bidi/agent/__init__.py (100%) rename strands-py/tests/strands/{experimental => }/bidi/agent/test_agent.py (98%) rename strands-py/tests/strands/{experimental => }/bidi/agent/test_loop.py (99%) rename strands-py/tests/strands/{experimental => }/bidi/agent/test_loop_telemetry.py (98%) rename strands-py/tests/strands/{experimental => }/bidi/agent/test_reconnect_timer.py (97%) rename strands-py/tests/strands/{experimental => }/bidi/agent/test_snapshot.py (98%) rename strands-py/tests/strands/{experimental => }/bidi/conftest.py (56%) rename strands-py/tests/strands/{experimental => }/bidi/hooks/__init__.py (100%) rename strands-py/tests/strands/{experimental => }/bidi/hooks/test_events.py (94%) rename strands-py/tests/strands/{experimental => }/bidi/io/__init__.py (100%) rename strands-py/tests/strands/{experimental => }/bidi/io/conftest.py (100%) rename strands-py/tests/strands/{experimental => }/bidi/io/console/test_io.py (98%) rename strands-py/tests/strands/{experimental => }/bidi/io/console/test_keyboard.py (94%) rename strands-py/tests/strands/{experimental => }/bidi/io/test_audio.py (97%) rename strands-py/tests/strands/{experimental => }/bidi/models/__init__.py (100%) rename strands-py/tests/strands/{experimental => }/bidi/models/test_bedrock.py (99%) rename strands-py/tests/strands/{experimental => }/bidi/models/test_configs.py (94%) rename strands-py/tests/strands/{experimental => }/bidi/models/test_google.py (99%) rename strands-py/tests/strands/{experimental => }/bidi/models/test_model.py (93%) rename strands-py/tests/strands/{experimental => }/bidi/models/test_openai.py (99%) rename strands-py/tests/strands/{experimental => }/bidi/types/__init__.py (100%) rename strands-py/tests/strands/{experimental => }/bidi/types/test_events.py (98%) create mode 100644 strands-py/tests/strands/experimental/bidi/test_deprecated_alias.py create mode 100644 strands-py/tests_typing/bidi/test_deprecated_alias.py diff --git a/site/SITE-ARCHITECTURE.md b/site/SITE-ARCHITECTURE.md index e354b29a1..397034681 100644 --- a/site/SITE-ARCHITECTURE.md +++ b/site/SITE-ARCHITECTURE.md @@ -421,7 +421,7 @@ The index page (`src/content/docs/api/python/index.mdx`) is a permanent file (no ``` strands.agent.agent → Agent > Agent strands.agent.base → Agent > Base -strands.experimental.bidi.types → Experimental > Bidi > Types +strands.bidi.types → Bidi > Types ``` ### Index Page Component (`src/components/PythonApiList.astro`) diff --git a/site/scripts/api-generation-python.py b/site/scripts/api-generation-python.py index 3791add0e..951ae885f 100644 --- a/site/scripts/api-generation-python.py +++ b/site/scripts/api-generation-python.py @@ -27,7 +27,9 @@ from pydoc_markdown.contrib.processors.smart import SmartProcessor from pydoc_markdown.contrib.renderers.markdown import MarkdownRenderer from pydoc_markdown.contrib.source_linkers.git import GitSourceLinker -BIDI_PACKAGE = "strands.experimental.bidi" +BIDI_PACKAGE = "strands.bidi" +BIDI_PUBLIC_MODULES = ("agent", "hooks", "io", "models", "types") +DEPRECATED_BIDI_PACKAGE = "strands.experimental.bidi" def _read_module_tree(source_root: Path, module_name: str) -> ast.Module: @@ -113,11 +115,9 @@ def _read_public_sources(source_root: Path, module_name: str) -> dict[str, set[s def _read_bidi_public_api(source_root: Path) -> dict[str, dict[str, set[str]]]: - package_tree = _read_module_tree(source_root, BIDI_PACKAGE) - owner_names = _read_all_exports(package_tree, BIDI_PACKAGE) return { f"{BIDI_PACKAGE}.{owner_name}": _read_public_sources(source_root, f"{BIDI_PACKAGE}.{owner_name}") - for owner_name in owner_names + for owner_name in BIDI_PUBLIC_MODULES } @@ -208,6 +208,9 @@ def generate_docs(): for module in modules: module_name = module.name + if module_name == DEPRECATED_BIDI_PACKAGE or module_name.startswith(f"{DEPRECATED_BIDI_PACKAGE}."): + continue + if module_name in bidi_public_api: continue diff --git a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/agent.mdx b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/agent.mdx index b1821de42..5e52bf970 100644 --- a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/agent.mdx +++ b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/agent.mdx @@ -1,12 +1,11 @@ --- title: BidiAgent description: 'Build real-time voice conversations with BidiAgent. Stream audio and text over persistent connections with barge-ins and concurrent tool calling.' -experimental: true tags: [bidi-streaming] sourceLinks: - - path: strands-py/src/strands/experimental/bidi/agent/agent.py - - path: strands-py/src/strands/experimental/bidi/agent/loop.py - - path: strands-py/src/strands/experimental/bidi/types/content.py + - path: strands-py/src/strands/bidi/agent/agent.py + - path: strands-py/src/strands/bidi/agent/loop.py + - path: strands-py/src/strands/bidi/types/content.py --- @@ -69,9 +68,9 @@ print(result.message) ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import AudioIO -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.io import AudioIO +from strands.bidi.models import BedrockNovaSonicModel model = BedrockNovaSonicModel(model_id="amazon.nova-2-sonic-v1:0") agent = BidiAgent(model=model, tools=[notebook]) @@ -202,8 +201,8 @@ Configure a `BidiAgent` with a model, tools, a system prompt, and optional conve ### Basic Configuration ```python -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.models import BedrockNovaSonicModel model = BedrockNovaSonicModel(model_id="amazon.nova-2-sonic-v1:0") @@ -223,7 +222,7 @@ agent = BidiAgent( Each model provider has specific configuration options: ```python -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.models import BedrockNovaSonicModel model = BedrockNovaSonicModel( model_id="amazon.nova-2-sonic-v1:0", @@ -356,7 +355,7 @@ Send a list to group text and images into one user message, preserving block ord With a running OpenAI or Gemini agent: ```python -from strands.experimental.bidi.agent import BidiAgent +from strands.bidi.agent import BidiAgent from strands.types.media import ImageBlock async def describe_image(agent: BidiAgent, image_bytes: bytes) -> None: @@ -400,7 +399,7 @@ Each provider declares its reconnect timing as a `ConnectionConfig`. Override it opt out of automatic reconnect with the model's `connection` argument: ```python -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.models import BedrockNovaSonicModel model = BedrockNovaSonicModel( model_id="amazon.nova-2-sonic-v1:0", @@ -486,5 +485,5 @@ and event queues, then invokes cleanup hooks. - [I/O Streams](io.md) - Building custom input and output streams - [Model Providers](models/bedrock.md) - Provider-specific configuration - [Quickstart](quickstart.md) - Getting started guide -- [Python API Reference](@api/python/strands.experimental.bidi.agent) - +- [Python API Reference](@api/python/strands.bidi.agent) - Complete API documentation diff --git a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/barge-in.mdx b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/barge-in.mdx index cc30357cb..c191d48ba 100644 --- a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/barge-in.mdx +++ b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/barge-in.mdx @@ -1,14 +1,13 @@ --- title: Barge-in description: 'Handle real-time voice barge-ins in BidiAgent. Voice Activity Detection stops responses mid-stream for natural, human-like conversations.' -experimental: true tags: [bidi-streaming] redirectFrom: - docs/user-guide/concepts/bidirectional-streaming/interruption - docs/user-guide/sdk/bidirectional-streaming/interruption sourceLinks: - - path: strands-py/src/strands/experimental/bidi/types/events.py - - path: strands-py/src/strands/experimental/bidi/io/audio.py + - path: strands-py/src/strands/bidi/types/events.py + - path: strands-py/src/strands/bidi/io/audio.py --- @@ -41,9 +40,9 @@ When using `AudioIO`, barge-ins are handled automatically: ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import AudioIO -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.io import AudioIO +from strands.bidi.models import BedrockNovaSonicModel model = BedrockNovaSonicModel(model_id="amazon.nova-2-sonic-v1:0") agent = BidiAgent(model=model) @@ -68,9 +67,9 @@ For custom behavior, process barge-in events manually: ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.models import BedrockNovaSonicModel -from strands.experimental.bidi.types import BidiBargeInEvent +from strands.bidi.agent import BidiAgent +from strands.bidi.models import BedrockNovaSonicModel +from strands.bidi.types import BidiBargeInEvent model = BedrockNovaSonicModel(model_id="amazon.nova-2-sonic-v1:0") agent = BidiAgent(model=model) @@ -104,8 +103,8 @@ asyncio.run(main()) Use hooks to track barge-ins across your application: ```python -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.hooks import ( +from strands.bidi.agent import BidiAgent +from strands.bidi.hooks import ( BidiBargeInEvent as BidiBargeInHookEvent, ) @@ -135,7 +134,7 @@ agent = BidiAgent( If barge-ins aren't being detected: ```python -from strands.experimental.bidi.models import OpenAIRealtimeModel +from strands.bidi.models import OpenAIRealtimeModel # Check VAD configuration (OpenAI) model = OpenAIRealtimeModel( @@ -178,7 +177,7 @@ async def __call__(self, event: BidiOutputEvent): If barge-in is detected too easily: ```python -from strands.experimental.bidi.models import OpenAIRealtimeModel +from strands.bidi.models import OpenAIRealtimeModel # Increase VAD threshold (OpenAI) model = OpenAIRealtimeModel( diff --git a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/events.mdx b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/events.mdx index d1eb5a4b7..2fcd810bb 100644 --- a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/events.mdx +++ b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/events.mdx @@ -1,15 +1,14 @@ --- title: Events description: 'Send and receive bidirectional streaming events in Strands agents. Process audio, text, and tool activity in real time over persistent connections.' -experimental: true tags: [bidi-streaming] sourceLinks: - - path: strands-py/src/strands/experimental/bidi/types/agent.py - - path: strands-py/src/strands/experimental/bidi/types/content.py - - path: strands-py/src/strands/experimental/bidi/types/media.py + - path: strands-py/src/strands/bidi/types/agent.py + - path: strands-py/src/strands/bidi/types/content.py + - path: strands-py/src/strands/bidi/types/media.py - path: strands-py/src/strands/types/content.py - - path: strands-py/src/strands/experimental/bidi/types/events.py - - path: strands-py/src/strands/experimental/bidi/types/io.py + - path: strands-py/src/strands/bidi/types/events.py + - path: strands-py/src/strands/bidi/types/io.py - path: strands-py/src/strands/types/media.py --- @@ -35,8 +34,8 @@ Bidirectional streaming uses a different event model than [standard streaming](. ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.models import BedrockNovaSonicModel async def main(): model = BedrockNovaSonicModel(model_id="amazon.nova-2-sonic-v1:0") @@ -84,7 +83,7 @@ determines the sample rate and channel count for real-time PCM audio. ```python from pathlib import Path -from strands.experimental.bidi.types import AudioDelta +from strands.bidi.types import AudioDelta audio_bytes = Path("audio-chunk.pcm").read_bytes() @@ -427,8 +426,8 @@ Use deltas for live updates and block events for completed text, reasoning, and transcripts. This function reads completed content from a started agent: ```python -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.types import ( +from strands.bidi.agent import BidiAgent +from strands.bidi.types import ( BidiReasoningBlockEvent, BidiTextBlockEvent, BidiTranscriptBlockEvent, @@ -558,9 +557,9 @@ Emitted periodically to report token usage with modality breakdown. ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import AudioIO -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.io import AudioIO +from strands.bidi.models import BedrockNovaSonicModel async def main(): model = BedrockNovaSonicModel(model_id="amazon.nova-2-sonic-v1:0") @@ -595,8 +594,8 @@ asyncio.run(main()) ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.models import BedrockNovaSonicModel async def main(): model = BedrockNovaSonicModel(model_id="amazon.nova-2-sonic-v1:0") @@ -615,8 +614,8 @@ asyncio.run(main()) ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.models import BedrockNovaSonicModel from strands.vended_tools import notebook async def main(): @@ -644,8 +643,8 @@ asyncio.run(main()) ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.models import BedrockNovaSonicModel async def main(): model = BedrockNovaSonicModel(model_id="amazon.nova-2-sonic-v1:0") @@ -670,8 +669,8 @@ asyncio.run(main()) ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.models import BedrockNovaSonicModel async def main(): model = BedrockNovaSonicModel(model_id="amazon.nova-2-sonic-v1:0") # 8-minute timeout diff --git a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/hooks.mdx b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/hooks.mdx index 5f7b186aa..5aad441fb 100644 --- a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/hooks.mdx +++ b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/hooks.mdx @@ -3,12 +3,11 @@ title: Bidirectional Streaming Hooks description: 'Extend BidiAgent with hooks for bidirectional streaming events: connection lifecycle, barge-ins, restarts, and real-time conversation logging.' sidebar: label: "Hooks" -experimental: true tags: [bidi-streaming, hooks] sourceLinks: - - path: strands-py/src/strands/experimental/bidi/hooks/events.py - - path: strands-py/src/strands/experimental/bidi/agent/agent.py - - path: strands-py/src/strands/experimental/bidi/agent/loop.py + - path: strands-py/src/strands/bidi/hooks/events.py + - path: strands-py/src/strands/bidi/agent/agent.py + - path: strands-py/src/strands/bidi/agent/loop.py - path: strands-py/src/strands/hooks/events.py - path: strands-py/src/strands/types/agent.py --- @@ -42,8 +41,8 @@ Register related hooks together by implementing `register_hooks()`: ```python from strands import LocalAgent -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.hooks import BidiAgentStopEvent, BidiResponseStopEvent +from strands.bidi.agent import BidiAgent +from strands.bidi.hooks import BidiAgentStopEvent, BidiResponseStopEvent from strands.hooks import AgentInitializedEvent, HookRegistry, MessageAddedEvent @@ -79,7 +78,7 @@ Register a single hook with `add_hook()`, which infers the event type: ```python from strands import LocalAgent -from strands.experimental.bidi.agent import BidiAgent +from strands.bidi.agent import BidiAgent from strands.hooks import MessageAddedEvent @@ -100,7 +99,7 @@ their default agent type. To observe completed transcripts, subscribe to `MessageUpdatedEvent`. A transcript first appears as an empty message through `MessageAddedEvent` when the transcript starts. Its completion replaces that message at the reserved position. `event.tracking_id` identifies the message and `event.message` contains the replacement. ```python -from strands.experimental.bidi.agent import BidiAgent +from strands.bidi.agent import BidiAgent from strands.hooks import MessageUpdatedEvent @@ -120,7 +119,7 @@ for their agent type: ```python from strands import LocalAgent, ToolContext, tool -from strands.experimental.bidi.agent import BidiAgent +from strands.bidi.agent import BidiAgent from strands.hooks import AfterToolCallEvent, BeforeToolCallEvent @@ -196,8 +195,8 @@ The hook mirrors model-reported completion: shutdown or a connection failure wit a stop event does not emit it. The hook and streaming event share a name but are separate classes. Import the hook -from `strands.experimental.bidi.hooks`. Import the streaming event from -`strands.experimental.bidi.types` when handling `agent.receive()` output. +from `strands.bidi.hooks`. Import the streaming event from +`strands.bidi.types` when handling `agent.receive()` output. ## Cookbook @@ -208,8 +207,8 @@ This section contains practical hook implementations for common use cases. Count barge-ins and record their reasons: ```python -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.hooks import BidiBargeInEvent +from strands.bidi.agent import BidiAgent +from strands.bidi.hooks import BidiBargeInEvent from strands.hooks import HookRegistry @@ -234,8 +233,8 @@ agent = BidiAgent(hooks=[tracker]) Track connection restart attempts and their outcomes: ```python -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.hooks import ( +from strands.bidi.agent import BidiAgent +from strands.bidi.hooks import ( BidiAfterConnectionRestartEvent, BidiBeforeConnectionRestartEvent, ) @@ -269,8 +268,8 @@ agent = BidiAgent(hooks=[ConnectionMonitor()]) Count model-reported response completions and report the total when the agent stops: ```python -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.hooks import BidiAgentStopEvent, BidiResponseStopEvent +from strands.bidi.agent import BidiAgent +from strands.bidi.hooks import BidiAgentStopEvent, BidiResponseStopEvent from strands.hooks import HookRegistry @@ -301,7 +300,7 @@ across connection restarts. Changes made by any of them are visible to the other ```python from strands import LocalAgent, tool -from strands.experimental.bidi.agent import BidiAgent +from strands.bidi.agent import BidiAgent from strands.hooks import BeforeToolCallEvent @@ -338,4 +337,4 @@ For more guidance on performance, errors, and composition, see the - [Agent](agent.md) - Learn about BidiAgent configuration and lifecycle - [Events](events.md) - Complete guide to bidirectional streaming events -- [Python API Reference](@api/python/strands.experimental.bidi.agent) - Complete API documentation +- [Python API Reference](@api/python/strands.bidi.agent) - Complete API documentation diff --git a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/index.mdx b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/index.mdx index 231fc37e2..cd83a1e37 100644 --- a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/index.mdx +++ b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/index.mdx @@ -1,7 +1,6 @@ --- title: Build a realtime voice agent description: "Build a realtime agent that streams audio in and out: a persistent BidiAgent connection, a realtime model provider, and the events, I/O, and barge-ins a live conversation needs." -experimental: true tags: [bidi-streaming] redirectFrom: - docs/user-guide/concepts/bidirectional-streaming/session-management @@ -85,9 +84,9 @@ loop streams audio both ways until you interrupt it. ```python import asyncio -from strands.experimental.bidi import BidiAgent -from strands.experimental.bidi.io import AudioIO -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi import BidiAgent +from strands.bidi.io import AudioIO +from strands.bidi.models import BedrockNovaSonicModel model = BedrockNovaSonicModel(model_id="amazon.nova-2-sonic-v1:0") agent = BidiAgent( @@ -105,7 +104,7 @@ async def main(): asyncio.run(main()) ``` -Bidirectional streaming is a Python-only experimental feature; install it with +Bidirectional streaming is available in the Python SDK. Install it with `pip install "strands-agents[bidi-all]"`. The [quickstart](quickstart/) covers per-provider installs and credentials. diff --git a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/io.mdx b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/io.mdx index 6c16825d5..b110a51e8 100644 --- a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/io.mdx +++ b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/io.mdx @@ -5,13 +5,12 @@ description: >- with input and output streams for audio and text. sidebar: label: "IO" -experimental: true tags: [bidi-streaming] sourceLinks: - - path: strands-py/src/strands/experimental/bidi/io/console/_io.py - - path: strands-py/src/strands/experimental/bidi/io/console/_display.py - - path: strands-py/src/strands/experimental/bidi/io/console/_keyboard.py - - path: strands-py/src/strands/experimental/bidi/io/audio.py + - path: strands-py/src/strands/bidi/io/console/_io.py + - path: strands-py/src/strands/bidi/io/console/_display.py + - path: strands-py/src/strands/bidi/io/console/_keyboard.py + - path: strands-py/src/strands/bidi/io/audio.py --- @@ -46,10 +45,10 @@ management. Implementation of these protocols will look as follows: ```python -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.types import BidiAgentInput -from strands.experimental.bidi.types import BidiOutputEvent -from strands.experimental.bidi.types import InputStream, OutputStream +from strands.bidi.agent import BidiAgent +from strands.bidi.types import BidiAgentInput +from strands.bidi.types import BidiOutputEvent +from strands.bidi.types import InputStream, OutputStream class MyInputStream(InputStream): @@ -87,7 +86,7 @@ Pass your I/O streams to the agent's `run()` method to connect them to the agent ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent +from strands.bidi.agent import BidiAgent async def main(): @@ -124,8 +123,8 @@ PyAudio is excluded from the aggregate `bidi-all` extra because of its PortAudio ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import AudioIO +from strands.bidi.agent import BidiAgent +from strands.bidi.io import AudioIO async def main(): @@ -174,7 +173,7 @@ pip install "strands-agents[bidi,bidi-pyaudio,bidi-aec]" Pass `audio_processor=True` for the defaults, or an `AudioProcessorConfig` to tune it: ```python -from strands.experimental.bidi.io import AudioIO, AudioProcessorConfig +from strands.bidi.io import AudioIO, AudioProcessorConfig # Echo cancellation, noise suppression, and auto gain control with defaults: audio_io = AudioIO(audio_processor=True) @@ -224,9 +223,9 @@ pip install "strands-agents[bidi-io,bidi-openai]" ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import ConsoleIO -from strands.experimental.bidi.models import OpenAIRealtimeModel +from strands.bidi.agent import BidiAgent +from strands.bidi.io import ConsoleIO +from strands.bidi.models import OpenAIRealtimeModel async def main(): @@ -273,8 +272,8 @@ This example uses Amazon Nova Sonic. Configure ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import AudioIO, ConsoleIO +from strands.bidi.agent import BidiAgent +from strands.bidi.io import AudioIO, ConsoleIO async def main(): @@ -307,8 +306,8 @@ shows how to use WebSockets with `run()`: # server.py from fastapi import FastAPI, WebSocket, WebSocketDisconnect -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.models import OpenAIRealtimeModel +from strands.bidi.agent import BidiAgent +from strands.bidi.models import OpenAIRealtimeModel app = FastAPI() diff --git a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/models/bedrock.mdx b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/models/bedrock.mdx index c22870aaf..8f5d18fc0 100644 --- a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/models/bedrock.mdx +++ b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/models/bedrock.mdx @@ -1,10 +1,9 @@ --- title: Bedrock Nova Sonic description: 'Build real-time speech-to-speech agents with Amazon Bedrock Nova Sonic and Strands. Bidirectional audio streaming with barge-in handling and tool calling.' -experimental: true tags: [bidi-streaming, aws, bedrock] sourceLinks: - - path: strands-py/src/strands/experimental/bidi/models/bedrock.py + - path: strands-py/src/strands/bidi/models/bedrock.py redirectFrom: - docs/user-guide/concepts/bidirectional-streaming/models/nova_sonic --- @@ -46,9 +45,9 @@ After installing the Bedrock Nova Sonic and local audio extras, create a voice a ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import AudioIO -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.io import AudioIO +from strands.bidi.models import BedrockNovaSonicModel from strands.vended_tools import notebook @@ -101,7 +100,7 @@ export AWS_REGION=your_region_name ```python import boto3 -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.models import BedrockNovaSonicModel boto_session = boto3.Session( @@ -133,10 +132,10 @@ For more details on this approach, please refer to the [boto3 session docs](http | Parameter | Description | Example | Options | | --------- | ----------- | ------- | ------- | | `model_id` | Nova Sonic model identifier. | `"amazon.nova-2-sonic-v1:0"` | Nova Sonic model IDs | -| `audio` | Input and output stream options. | `{"output": {"sample_rate": 24000}}` | [reference](@api/python/strands.experimental.bidi.models#BedrockNovaSonicAudioConfig) | +| `audio` | Input and output stream options. | `{"output": {"sample_rate": 24000}}` | [reference](@api/python/strands.bidi.models#BedrockNovaSonicAudioConfig) | | `voice` | Output voice identifier. Defaults to `"matthew"`. | `"tiffany"` | Nova Sonic voices | | `params` | Provider-specific session parameters, such as inference and turn detection configuration. | `{"inferenceConfiguration": {"temperature": 0.7}}` | [`sessionStart` fields](https://docs.aws.amazon.com/nova/latest/nova2-userguide/sonic-input-events.html) | -| `connection` | Reconnect timing overrides. | `{"auto_reconnect": False}` | [reference](@api/python/strands.experimental.bidi.models#ConnectionConfig) | +| `connection` | Reconnect timing overrides. | `{"auto_reconnect": False}` | [reference](@api/python/strands.bidi.models#ConnectionConfig) | :::caution[Conversation History Limits] Nova Sonic caps conversation history passed via `messages` at 50KB per message and 200KB total. Messages exceeding the per-message limit are truncated; if the total exceeds 200KB, the oldest messages are dropped until it fits. This happens silently (only visible via debug-level logs) and only applies to history provided at connection start, not to turns generated during the live conversation. @@ -168,4 +167,4 @@ As a reminder, Nova Sonic is only available in us-east-1, us-west-2, eu-north-1, - [Nova Sonic](https://docs.aws.amazon.com/nova/latest/nova2-userguide/using-conversational-speech.html) - [Experimental Bedrock Client](https://github.com/aws/aws-sdk-python/tree/develop/clients/aws-sdk-bedrock-runtime) -- [Python API Reference](@api/python/strands.experimental.bidi.models#BedrockNovaSonicModel) +- [Python API Reference](@api/python/strands.bidi.models#BedrockNovaSonicModel) diff --git a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/models/google.mdx b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/models/google.mdx index 09d460af3..4f86895d0 100644 --- a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/models/google.mdx +++ b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/models/google.mdx @@ -1,10 +1,9 @@ --- title: Google Gemini Live description: "Build real-time voice agents with Google's Gemini Live API and Strands. Stream audio and text over WebSocket with barge-ins and tool calling." -experimental: true tags: [bidi-streaming] sourceLinks: - - path: strands-py/src/strands/experimental/bidi/models/google.py + - path: strands-py/src/strands/bidi/models/google.py redirectFrom: - docs/user-guide/concepts/bidirectional-streaming/models/gemini_live --- @@ -42,9 +41,9 @@ After installing the Gemini Live and local audio extras, create a voice agent: ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import AudioIO -from strands.experimental.bidi.models import GoogleGeminiLiveModel +from strands.bidi.agent import BidiAgent +from strands.bidi.io import AudioIO +from strands.bidi.models import GoogleGeminiLiveModel from strands.vended_tools import notebook @@ -76,10 +75,10 @@ the [Google GenAI client reference](https://googleapis.github.io/python-genai/ge | Parameter | Description | Example | Options | | --------- | ----------- | ------- | ------- | | `model_id` | Gemini Live model identifier. | `"gemini-3.8-live"` | [Gemini models](https://ai.google.dev/gemini-api/docs/models) | -| `audio` | Input audio options. | `{"input": {"sample_rate": 48000}}` | [reference](@api/python/strands.experimental.bidi.models#GoogleGeminiLiveAudioConfig) | +| `audio` | Input audio options. | `{"input": {"sample_rate": 48000}}` | [reference](@api/python/strands.bidi.models#GoogleGeminiLiveAudioConfig) | | `voice` | Prebuilt output voice name. Uses the provider default when omitted. | `"Kore"` | [Voices and languages](https://docs.cloud.google.com/text-to-speech/docs/list-voices-and-types) | | `params` | Gemini Live session parameters. | `{"temperature": 0.7}` | [`LiveConnectConfig`](https://googleapis.github.io/python-genai/genai.html#genai.types.LiveConnectConfig) | -| `connection` | Reconnect timing overrides. | `{"auto_reconnect": False}` | [reference](@api/python/strands.experimental.bidi.models#ConnectionConfig) | +| `connection` | Reconnect timing overrides. | `{"auto_reconnect": False}` | [reference](@api/python/strands.bidi.models#ConnectionConfig) | ### Additional Provider Options @@ -87,7 +86,7 @@ Use direct options such as `voice` for common settings. For additional Google Ge options, pass `params` using snake_case field names. ```python -from strands.experimental.bidi.models import GoogleGeminiLiveModel +from strands.bidi.models import GoogleGeminiLiveModel model = GoogleGeminiLiveModel( model_id="gemini-3.8-live", @@ -124,4 +123,4 @@ environment variable. You can obtain an API key from - [Gemini Live API](https://ai.google.dev/gemini-api/docs/live) - [Gemini API Reference](https://googleapis.github.io/python-genai/genai.html#) -- [Python API Reference](@api/python/strands.experimental.bidi.models#GoogleGeminiLiveModel) +- [Python API Reference](@api/python/strands.bidi.models#GoogleGeminiLiveModel) diff --git a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/models/openai.mdx b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/models/openai.mdx index bd900544d..09211a961 100644 --- a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/models/openai.mdx +++ b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/models/openai.mdx @@ -1,10 +1,9 @@ --- title: OpenAI Realtime description: 'Build low-latency voice agents with the OpenAI Realtime API and Strands. Configure speech-to-speech streaming, barge-ins, and tool calling.' -experimental: true tags: [bidi-streaming] sourceLinks: - - path: strands-py/src/strands/experimental/bidi/models/openai.py + - path: strands-py/src/strands/bidi/models/openai.py redirectFrom: - docs/user-guide/concepts/bidirectional-streaming/models/openai_realtime --- @@ -41,9 +40,9 @@ After installing the OpenAI Realtime and local audio extras, create a voice agen ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import AudioIO -from strands.experimental.bidi.models import OpenAIRealtimeModel +from strands.bidi.agent import BidiAgent +from strands.bidi.io import AudioIO +from strands.bidi.models import OpenAIRealtimeModel from strands.vended_tools import notebook @@ -83,14 +82,14 @@ if __name__ == "__main__": | `transcription_model_id` | Required input transcription model identifier. Pass `None` to disable user transcription. | `"gpt-transcribe"` | [GPT-Transcribe](https://developers.openai.com/api/docs/models/gpt-transcribe) | | `voice` | Output voice identifier. Defaults to `"alloy"`. | `"coral"` | [Voice options](https://platform.openai.com/docs/guides/realtime-conversations#voice-options) | | `params` | OpenAI Realtime session parameters. Audio must remain mono PCM at 24000 Hz. | `{"max_output_tokens": 4096}` | [`session.update`](https://platform.openai.com/docs/api-reference/realtime-client-events/session/update) | -| `connection` | Reconnect timing overrides. | `{"auto_reconnect": False}` | [reference](@api/python/strands.experimental.bidi.models#ConnectionConfig) | +| `connection` | Reconnect timing overrides. | `{"auto_reconnect": False}` | [reference](@api/python/strands.bidi.models#ConnectionConfig) | ### Additional Provider Options Use direct options such as `voice` and `transcription_model_id` for common settings, and pass additional OpenAI Realtime options through `params`. ```python -from strands.experimental.bidi.models import OpenAIRealtimeModel +from strands.bidi.models import OpenAIRealtimeModel model = OpenAIRealtimeModel( model_id="gpt-realtime-2.1", @@ -128,4 +127,4 @@ Set the `OPENAI_API_KEY` environment variable or pass the key through `api_key`. - [OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime) - [OpenAI API Reference](https://platform.openai.com/docs/api-reference/realtime) -- [Python API Reference](@api/python/strands.experimental.bidi.models#OpenAIRealtimeModel) +- [Python API Reference](@api/python/strands.bidi.models#OpenAIRealtimeModel) diff --git a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/observability.mdx b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/observability.mdx index 6ac6fcef1..d3603dc8f 100644 --- a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/observability.mdx +++ b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/observability.mdx @@ -4,11 +4,10 @@ description: 'Trace, measure, and log BidiAgent sessions: connection lifecycle, sidebar: label: "Observability" languages: Python -experimental: true tags: [bidi-streaming, observability] sourceLinks: - - path: strands-py/src/strands/experimental/bidi/_telemetry.py - - path: strands-py/src/strands/experimental/bidi/agent/loop.py + - path: strands-py/src/strands/bidi/_telemetry.py + - path: strands-py/src/strands/bidi/agent/loop.py --- @@ -31,9 +30,9 @@ pip install 'strands-agents[bidi,otel]' ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.types import BidiConnectionStartEvent -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.types import BidiConnectionStartEvent +from strands.bidi.models import BedrockNovaSonicModel from strands.telemetry import StrandsTelemetry strands_telemetry = StrandsTelemetry() @@ -278,7 +277,7 @@ Register a hook provider to accumulate session health counters: ```python import logging -from strands.experimental.bidi.hooks import ( +from strands.bidi.hooks import ( BidiAfterConnectionRestartEvent, BidiBargeInEvent, ) @@ -315,8 +314,8 @@ Traces record aggregate session tokens. The per-modality breakdown is only avail the event stream: ```python -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.types import BidiResponseStopEvent, BidiUsageEvent +from strands.bidi.agent import BidiAgent +from strands.bidi.types import BidiResponseStopEvent, BidiUsageEvent async def track_session(agent: BidiAgent) -> None: @@ -357,7 +356,7 @@ behavior the trace does not explain, such as a tool call that never appears to e ```python import logging -logging.getLogger("strands.experimental.bidi").setLevel(logging.DEBUG) +logging.getLogger("strands.bidi").setLevel(logging.DEBUG) logging.basicConfig( format="%(levelname)s | %(name)s | %(message)s", handlers=[logging.StreamHandler()], @@ -367,13 +366,13 @@ logging.basicConfig( A session that starts, calls a tool, and stops logs this: ``` -DEBUG | strands.experimental.bidi.agent.agent | context_manager= | starting agent -DEBUG | strands.experimental.bidi.agent.agent | agent starting -DEBUG | strands.experimental.bidi.agent.loop | agent loop starting -DEBUG | strands.experimental.bidi.agent.loop | model task starting -DEBUG | strands.experimental.bidi.agent.loop | tool_name= | tool execution starting -DEBUG | strands.experimental.bidi.agent.agent | context_manager= | stopping agent -DEBUG | strands.experimental.bidi.agent.loop | agent loop stopping +DEBUG | strands.bidi.agent.agent | context_manager= | starting agent +DEBUG | strands.bidi.agent.agent | agent starting +DEBUG | strands.bidi.agent.loop | agent loop starting +DEBUG | strands.bidi.agent.loop | model task starting +DEBUG | strands.bidi.agent.loop | tool_name= | tool execution starting +DEBUG | strands.bidi.agent.agent | context_manager= | stopping agent +DEBUG | strands.bidi.agent.loop | agent loop stopping ``` Audio and transcript chunks are not logged, since a minute of conversation produces @@ -465,4 +464,4 @@ Each provider emits one span per response. Usage details vary by provider: - [Events](events.md) - Complete guide to bidirectional streaming events - [Hooks](hooks.md) - Extend agent functionality with hooks - [Barge-in](barge-in.md) - How barge-in detection works -- [Python API Reference](@api/python/strands.experimental.bidi.agent) - Complete API documentation +- [Python API Reference](@api/python/strands.bidi.agent) - Complete API documentation diff --git a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/quickstart.mdx b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/quickstart.mdx index 5e4ebfb02..38ab9f794 100644 --- a/site/src/content/docs/user-guide/sdk/bidirectional-streaming/quickstart.mdx +++ b/site/src/content/docs/user-guide/sdk/bidirectional-streaming/quickstart.mdx @@ -2,7 +2,6 @@ title: Build a voice agent tags: [bidi-streaming, quickstart] description: "Build voice-enabled AI agents with real-time audio streaming. Works with Amazon Nova Sonic, Gemini Live, and OpenAI Realtime." -experimental: true --- @@ -22,7 +21,7 @@ Before starting, ensure you have: ## Install the SDK -Bidirectional streaming is included in the Strands Agents SDK as an experimental feature. Install the SDK with bidirectional streaming support: +Install the SDK with bidirectional streaming support: ### For All Providers @@ -153,9 +152,9 @@ Now let's create a simple voice-enabled agent that can have real-time conversati ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import AudioIO -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.io import AudioIO +from strands.bidi.models import BedrockNovaSonicModel # Create a bidirectional streaming model model = BedrockNovaSonicModel(model_id="amazon.nova-2-sonic-v1:0") @@ -203,9 +202,9 @@ The `run()` method runs indefinitely by default. The simplest way to stop conver ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import AudioIO -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.io import AudioIO +from strands.bidi.models import BedrockNovaSonicModel async def main(): model = BedrockNovaSonicModel(model_id="amazon.nova-2-sonic-v1:0") @@ -238,9 +237,9 @@ Just like standard Strands agents, bidirectional agents can use tools during con ```python import asyncio from strands import tool -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import AudioIO -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.io import AudioIO +from strands.bidi.models import BedrockNovaSonicModel from strands.vended_tools import notebook # Define a custom tool @@ -302,9 +301,9 @@ Choose supported audio settings on the model and device buffering on the I/O str ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import AudioIO -from strands.experimental.bidi.models import GoogleGeminiLiveModel +from strands.bidi.agent import BidiAgent +from strands.bidi.io import AudioIO +from strands.bidi.models import GoogleGeminiLiveModel # Configure model audio settings model = GoogleGeminiLiveModel( @@ -342,10 +341,10 @@ Bidirectional agents automatically handle barge-ins when users start speaking: ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import AudioIO -from strands.experimental.bidi.models import BedrockNovaSonicModel -from strands.experimental.bidi.types import BidiBargeInEvent +from strands.bidi.agent import BidiAgent +from strands.bidi.io import AudioIO +from strands.bidi.models import BedrockNovaSonicModel +from strands.bidi.types import BidiBargeInEvent model = BedrockNovaSonicModel(model_id="amazon.nova-2-sonic-v1:0") agent = BidiAgent(model=model) @@ -378,9 +377,9 @@ If you need more control over the agent lifecycle, you can manually call `start( ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.models import BedrockNovaSonicModel -from strands.experimental.bidi.types import BidiResponseStopEvent +from strands.bidi.agent import BidiAgent +from strands.bidi.models import BedrockNovaSonicModel +from strands.bidi.types import BidiResponseStopEvent async def main(): model = BedrockNovaSonicModel(model_id="amazon.nova-2-sonic-v1:0") @@ -411,9 +410,9 @@ To let users end a conversation by voice, define a tool that calls `agent.cancel ```python import asyncio from strands import LocalAgent, ToolContext, tool -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import AudioIO -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.io import AudioIO +from strands.bidi.models import BedrockNovaSonicModel @tool(context=True) @@ -451,9 +450,9 @@ To enable debug logs in your agent, configure the `strands` logger: ```python import asyncio import logging -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.io import AudioIO -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.agent import BidiAgent +from strands.bidi.io import AudioIO +from strands.bidi.models import BedrockNovaSonicModel # Enable debug logs logging.getLogger("strands").setLevel(logging.DEBUG) @@ -531,7 +530,7 @@ audio_io = AudioIO(input_device_index=1) Each provider caps how long a single connection stays open. Rather than wait for that limit, `BidiAgent` reconnects proactively: a timer fires ahead of the cap, the agent replays the conversation history into a fresh connection, and it emits a `BidiConnectionRestartEvent` with `reason="scheduled"`. If a connection times out first, the agent reconnects reactively and emits the same event with `reason="timeout"`. Treat both as informational, not errors: ```python -from strands.experimental.bidi.types import BidiConnectionRestartEvent +from strands.bidi.types import BidiConnectionRestartEvent async for event in agent.receive(): if isinstance(event, BidiConnectionRestartEvent): @@ -545,7 +544,7 @@ Providers declare reconnect timing through `ConnectionConfig`. Tune it, or opt o automatic reconnect with the model's `connection` argument: ```python -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.models import BedrockNovaSonicModel # Reconnect 60s earlier than the provider default model = BedrockNovaSonicModel( @@ -565,4 +564,4 @@ For longer sessions on a single connection, OpenAI Realtime allows a larger conn - [Nova Sonic](models/bedrock.md) - Amazon Bedrock's bidirectional streaming model - [OpenAI Realtime](models/openai.md) - OpenAI's Realtime API - [Gemini Live](models/google.md) - Google's Gemini Live API -- [Python API Reference](@api/python/strands.experimental.bidi.agent) - Complete API documentation +- [Python API Reference](@api/python/strands.bidi.agent) - Complete API documentation diff --git a/site/src/dynamic-sidebar.ts b/site/src/dynamic-sidebar.ts index 246591fc7..6a7a3aa92 100644 --- a/site/src/dynamic-sidebar.ts +++ b/site/src/dynamic-sidebar.ts @@ -46,7 +46,7 @@ export interface DocInfo { * * Example: * - strands.agent.agent -> Agent > Agent - * - strands.experimental.bidi.types -> Experimental > Bidi > Types + * - strands.bidi.types -> Bidi > Types */ export function buildPythonApiSidebar(docs: DocInfo[], currentSlug: string): SidebarEntry[] { const pythonApiDocs = docs.filter( diff --git a/site/src/util/api-counterparts.ts b/site/src/util/api-counterparts.ts index 491d0a5d0..cb4b07904 100644 --- a/site/src/util/api-counterparts.ts +++ b/site/src/util/api-counterparts.ts @@ -105,10 +105,9 @@ export function buildApiCounterpartMap(entries: readonly ApiDocEntry[]): Map Number(a.includes('.experimental.')) - Number(b.includes('.experimental.')) || diff --git a/site/src/util/api-link-converter.ts b/site/src/util/api-link-converter.ts index 906f62875..930a9b653 100644 --- a/site/src/util/api-link-converter.ts +++ b/site/src/util/api-link-converter.ts @@ -22,6 +22,13 @@ const PYTHON_API_PATTERN = /^(\.\.\/)*api-reference\/python\/([^#]+)\.md(#(.+))? */ const TS_API_PATTERN = /^(\.\.\/)*api-reference\/typescript\/(?:classes|interfaces)\/([^.]+)\.html(#(.+))?$/ +/** + * Map a strands.experimental.bidi module path to its strands.bidi equivalent. + */ +function resolveExperimentalBidiModule(modulePath: string): string { + return modulePath.replace(/^strands\.experimental\.bidi(?=\.|$)/, 'strands.bidi') +} + /** * Check if a link is an old-style API reference link that needs conversion. */ @@ -65,10 +72,10 @@ export function convertPythonApiLink(link: string): string | null { const symbolPart = hashContent.slice(modulePrefix.length) if (symbolPart.startsWith('.')) { // There's a symbol after the module path - return `@api/python/${modulePrefix}#${symbolPart.slice(1)}` + return `@api/python/${resolveExperimentalBidiModule(modulePrefix)}#${symbolPart.slice(1)}` } else if (symbolPart === '') { // Hash points to the module itself - return `@api/python/${modulePrefix}` + return `@api/python/${resolveExperimentalBidiModule(modulePrefix)}` } } @@ -85,7 +92,7 @@ export function convertPythonApiLink(link: string): string | null { } } - const modulePath = hashParts.slice(0, moduleEndIndex).join('.') + const modulePath = resolveExperimentalBidiModule(hashParts.slice(0, moduleEndIndex).join('.')) const symbol = hashParts.slice(moduleEndIndex).join('.') if (symbol) { @@ -95,7 +102,7 @@ export function convertPythonApiLink(link: string): string | null { } } else { // No hash - convert path to dotted module notation - const modulePath = 'strands.' + pathPart.split('/').join('.') + const modulePath = resolveExperimentalBidiModule('strands.' + pathPart.split('/').join('.')) return `@api/python/${modulePath}` } } diff --git a/site/src/util/redirect.ts b/site/src/util/redirect.ts index a9dcb5571..e894c22db 100644 --- a/site/src/util/redirect.ts +++ b/site/src/util/redirect.ts @@ -223,6 +223,13 @@ const PREFIX_SLUG_RULES: SlugRule[] = [ // and only genuinely-missing old SDK deep links fall through to /sdk/*. { match: startsWith('docs/user-guide/harness'), to: (m) => `docs/user-guide/sdk/${m[1]}` }, + // bidi graduated from strands.experimental.bidi to strands.bidi. API pages are generated, so + // STATIC_SLUG_REDIRECTS can't validate them as targets. + { + match: /^docs\/api\/python\/strands\.experimental\.bidi(\..+)?$/, + to: (m) => `docs/api/python/strands.bidi${m[1] ?? ''}`, + }, + // Changelog stream rename: the former "harness" stream is now the "sdk" stream. { match: startsWith('changelog/harness'), to: (m) => `changelog/sdk/${m[1]}` }, diff --git a/site/test/api-link-converter.test.ts b/site/test/api-link-converter.test.ts index 35db6abce..e7a8569c2 100644 --- a/site/test/api-link-converter.test.ts +++ b/site/test/api-link-converter.test.ts @@ -101,7 +101,7 @@ const BROKEN_LINKS_TEST_DATA: [string, string][] = [ ['../../../api-reference/python/agent/agent_result.md#strands.agent.agent_result', '@api/python/strands.agent.agent_result'], // user-guide/concepts/bidirectional-streaming/*.mdx - ['../../../api-reference/python/experimental/bidi/agent.md', '@api/python/strands.experimental.bidi.agent'], + ['../../../api-reference/python/experimental/bidi/agent.md', '@api/python/strands.bidi.agent'], // user-guide/concepts/multi-agent/graph.mdx ['../../../api-reference/python/multiagent/graph.md#strands.multiagent.graph.GraphNode', '@api/python/strands.multiagent.graph#GraphNode'], @@ -167,16 +167,16 @@ const BROKEN_LINKS_TEST_DATA: [string, string][] = [ ['../../../api-reference/python/types/tools.md#strands.types.tools.ToolUse', '@api/python/strands.types.tools#ToolUse'], // user-guide/concepts/bidirectional-streaming/models/google.mdx - ['../../../../api-reference/python/experimental/bidi/models.md#strands.experimental.bidi.models.AudioConfig', '@api/python/strands.experimental.bidi.models#AudioConfig'], - ['../../../../api-reference/python/experimental/bidi/models.md#strands.experimental.bidi.models.GoogleGeminiLiveModel', '@api/python/strands.experimental.bidi.models#GoogleGeminiLiveModel'], + ['../../../../api-reference/python/experimental/bidi/models.md#strands.experimental.bidi.models.AudioConfig', '@api/python/strands.bidi.models#AudioConfig'], + ['../../../../api-reference/python/experimental/bidi/models.md#strands.experimental.bidi.models.GoogleGeminiLiveModel', '@api/python/strands.bidi.models#GoogleGeminiLiveModel'], // user-guide/concepts/bidirectional-streaming/models/bedrock.mdx - ['../../../../api-reference/python/experimental/bidi/models.md#strands.experimental.bidi.models.AudioConfig', '@api/python/strands.experimental.bidi.models#AudioConfig'], - ['../../../../api-reference/python/experimental/bidi/models.md#strands.experimental.bidi.models.BedrockNovaSonicModel', '@api/python/strands.experimental.bidi.models#BedrockNovaSonicModel'], + ['../../../../api-reference/python/experimental/bidi/models.md#strands.experimental.bidi.models.AudioConfig', '@api/python/strands.bidi.models#AudioConfig'], + ['../../../../api-reference/python/experimental/bidi/models.md#strands.experimental.bidi.models.BedrockNovaSonicModel', '@api/python/strands.bidi.models#BedrockNovaSonicModel'], // user-guide/concepts/bidirectional-streaming/models/openai.mdx - ['../../../../api-reference/python/experimental/bidi/models.md#strands.experimental.bidi.models.AudioConfig', '@api/python/strands.experimental.bidi.models#AudioConfig'], - ['../../../../api-reference/python/experimental/bidi/models.md#strands.experimental.bidi.models.OpenAIRealtimeModel', '@api/python/strands.experimental.bidi.models#OpenAIRealtimeModel'], + ['../../../../api-reference/python/experimental/bidi/models.md#strands.experimental.bidi.models.AudioConfig', '@api/python/strands.bidi.models#AudioConfig'], + ['../../../../api-reference/python/experimental/bidi/models.md#strands.experimental.bidi.models.OpenAIRealtimeModel', '@api/python/strands.bidi.models#OpenAIRealtimeModel'], ] describe('API Link Converter', () => { @@ -238,7 +238,7 @@ describe('API Link Converter', () => { it('should convert bidi package paths', () => { expect(convertPythonApiLink('../api-reference/python/experimental/bidi/agent.md')).toBe( - '@api/python/strands.experimental.bidi.agent' + '@api/python/strands.bidi.agent' ) }) @@ -370,13 +370,13 @@ describe('API Link Converter', () => { convertApiLink( '../../../../api-reference/python/experimental/bidi/models.md#strands.experimental.bidi.models.AudioConfig' ) - ).toBe('@api/python/strands.experimental.bidi.models#AudioConfig') + ).toBe('@api/python/strands.bidi.models#AudioConfig') expect( convertApiLink( '../../../../api-reference/python/experimental/bidi/models.md#strands.experimental.bidi.models.GoogleGeminiLiveModel' ) - ).toBe('@api/python/strands.experimental.bidi.models#GoogleGeminiLiveModel') + ).toBe('@api/python/strands.bidi.models#GoogleGeminiLiveModel') }) // user-guide/concepts/tools/custom-tools.mdx @@ -397,7 +397,7 @@ describe('API Link Converter', () => { expect(convertApiLink('../../../api-reference/python/types/content.md')).toBe('@api/python/strands.types.content') expect(convertApiLink('../../../api-reference/python/experimental/bidi/agent.md')).toBe( - '@api/python/strands.experimental.bidi.agent' + '@api/python/strands.bidi.agent' ) }) }) diff --git a/site/test/update-docs.test.ts b/site/test/update-docs.test.ts index 29f3038f6..435586c58 100644 --- a/site/test/update-docs.test.ts +++ b/site/test/update-docs.test.ts @@ -74,7 +74,7 @@ Check out [GitHub](https://github.com/strands-agents/harness-sdk). it('should handle deeply nested relative paths', () => { const input = `See [AudioConfig](../../../../api-reference/python/experimental/bidi/models.md#strands.experimental.bidi.models.AudioConfig).` - const expected = `See [AudioConfig](@api/python/strands.experimental.bidi.models#AudioConfig).` + const expected = `See [AudioConfig](@api/python/strands.bidi.models#AudioConfig).` expect(convertApiLinks(input)).toBe(expected) }) diff --git a/strands-py/README.md b/strands-py/README.md index 24adbbc60..b17131cc5 100644 --- a/strands-py/README.md +++ b/strands-py/README.md @@ -232,9 +232,9 @@ pip install strands-agents[bidi,bidi-io,bidi-pyaudio] ```python import asyncio -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.models import BedrockNovaSonicModel -from strands.experimental.bidi.io import AudioIO +from strands.bidi.agent import BidiAgent +from strands.bidi.models import BedrockNovaSonicModel +from strands.bidi.io import AudioIO from strands_tools import calculator, stop async def main(): @@ -265,7 +265,7 @@ if __name__ == "__main__": **Configuration Options:** ```python -from strands.experimental.bidi.models import BedrockNovaSonicModel +from strands.bidi.models import BedrockNovaSonicModel # Configure audio streams and Nova Sonic session parameters. model = BedrockNovaSonicModel( diff --git a/strands-py/src/strands/_middleware/stages.py b/strands-py/src/strands/_middleware/stages.py index 3121b72ca..a90802717 100644 --- a/strands-py/src/strands/_middleware/stages.py +++ b/strands-py/src/strands/_middleware/stages.py @@ -13,7 +13,7 @@ from .types import MiddlewareStage if TYPE_CHECKING: from ..agent.agent import Agent - from ..experimental.bidi.agent import BidiAgent + from ..bidi.agent import BidiAgent from ..interrupt import _InterruptState from ..models.model import Model from ..types._events import EventLoopStopEvent, ModelStopReason, ToolResultEvent, TypedEvent diff --git a/strands-py/src/strands/bidi/__init__.py b/strands-py/src/strands/bidi/__init__.py new file mode 100644 index 000000000..f53974fac --- /dev/null +++ b/strands-py/src/strands/bidi/__init__.py @@ -0,0 +1,6 @@ +"""Bidirectional streaming APIs.""" + +from . import agent, hooks, io, models, types +from .agent import BidiAgent as BidiAgent + +__all__ = ["BidiAgent", "agent", "hooks", "io", "models", "types"] diff --git a/strands-py/src/strands/experimental/bidi/_async/__init__.py b/strands-py/src/strands/bidi/_async/__init__.py similarity index 100% rename from strands-py/src/strands/experimental/bidi/_async/__init__.py rename to strands-py/src/strands/bidi/_async/__init__.py diff --git a/strands-py/src/strands/experimental/bidi/_async/_task_group.py b/strands-py/src/strands/bidi/_async/_task_group.py similarity index 100% rename from strands-py/src/strands/experimental/bidi/_async/_task_group.py rename to strands-py/src/strands/bidi/_async/_task_group.py diff --git a/strands-py/src/strands/experimental/bidi/_async/_task_pool.py b/strands-py/src/strands/bidi/_async/_task_pool.py similarity index 100% rename from strands-py/src/strands/experimental/bidi/_async/_task_pool.py rename to strands-py/src/strands/bidi/_async/_task_pool.py diff --git a/strands-py/src/strands/experimental/bidi/_audio/__init__.py b/strands-py/src/strands/bidi/_audio/__init__.py similarity index 100% rename from strands-py/src/strands/experimental/bidi/_audio/__init__.py rename to strands-py/src/strands/bidi/_audio/__init__.py diff --git a/strands-py/src/strands/experimental/bidi/_audio/buffer.py b/strands-py/src/strands/bidi/_audio/buffer.py similarity index 100% rename from strands-py/src/strands/experimental/bidi/_audio/buffer.py rename to strands-py/src/strands/bidi/_audio/buffer.py diff --git a/strands-py/src/strands/experimental/bidi/_audio/processor.py b/strands-py/src/strands/bidi/_audio/processor.py similarity index 100% rename from strands-py/src/strands/experimental/bidi/_audio/processor.py rename to strands-py/src/strands/bidi/_audio/processor.py diff --git a/strands-py/src/strands/experimental/bidi/_telemetry.py b/strands-py/src/strands/bidi/_telemetry.py similarity index 97% rename from strands-py/src/strands/experimental/bidi/_telemetry.py rename to strands-py/src/strands/bidi/_telemetry.py index 11516a1a9..9725c87c2 100644 --- a/strands-py/src/strands/experimental/bidi/_telemetry.py +++ b/strands-py/src/strands/bidi/_telemetry.py @@ -2,13 +2,13 @@ Wraps the shared Tracer primitives with bidi-specific span creation and attribute names. Uses Tracer._start_span/_end_span directly rather than adding bidi-specific public methods -to the shared Tracer class, keeping experimental bidi telemetry self-contained. +to the shared Tracer class, keeping bidi telemetry self-contained. """ from opentelemetry.trace import Span -from ...telemetry.tracer import Tracer, serialize -from ...types.traces import AttributeValue +from ..telemetry.tracer import Tracer, serialize +from ..types.traces import AttributeValue def start_session_span( diff --git a/strands-py/src/strands/bidi/agent/__init__.py b/strands-py/src/strands/bidi/agent/__init__.py new file mode 100644 index 000000000..564973099 --- /dev/null +++ b/strands-py/src/strands/bidi/agent/__init__.py @@ -0,0 +1,5 @@ +"""Bidirectional agent for real-time streaming conversations.""" + +from .agent import BidiAgent + +__all__ = ["BidiAgent"] diff --git a/strands-py/src/strands/experimental/bidi/agent/_blocks.py b/strands-py/src/strands/bidi/agent/_blocks.py similarity index 98% rename from strands-py/src/strands/experimental/bidi/agent/_blocks.py rename to strands-py/src/strands/bidi/agent/_blocks.py index bbd887bb7..1c65b9e8f 100644 --- a/strands-py/src/strands/experimental/bidi/agent/_blocks.py +++ b/strands-py/src/strands/bidi/agent/_blocks.py @@ -3,7 +3,7 @@ from dataclasses import dataclass, field from typing import ClassVar, Literal -from ....types.content import Message, Role +from ...types.content import Message, Role from ..types.content import BidiContentMetadata from ..types.events import ( BidiOutputEvent, diff --git a/strands-py/src/strands/experimental/bidi/agent/_reconnect_timer.py b/strands-py/src/strands/bidi/agent/_reconnect_timer.py similarity index 100% rename from strands-py/src/strands/experimental/bidi/agent/_reconnect_timer.py rename to strands-py/src/strands/bidi/agent/_reconnect_timer.py diff --git a/strands-py/src/strands/experimental/bidi/agent/agent.py b/strands-py/src/strands/bidi/agent/agent.py similarity index 96% rename from strands-py/src/strands/experimental/bidi/agent/agent.py rename to strands-py/src/strands/bidi/agent/agent.py index 2b173496f..8913bc9d0 100644 --- a/strands-py/src/strands/experimental/bidi/agent/agent.py +++ b/strands-py/src/strands/bidi/agent/agent.py @@ -21,10 +21,10 @@ import uuid from collections.abc import AsyncGenerator from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast -from .... import _identifier -from ...._middleware import MiddlewareRegistry -from ....agent.state import AgentState -from ....hooks import ( +from ... import _identifier +from ..._middleware import MiddlewareRegistry +from ...agent.state import AgentState +from ...hooks import ( AgentInitializedEvent, HookCallback, HookOrder, @@ -33,18 +33,18 @@ from ....hooks import ( MessageAddedEvent, MessageUpdatedEvent, ) -from ....hooks.registry import TEvent -from ....interrupt import _InterruptState -from ....sandbox import Sandbox -from ....sandbox.not_a_sandbox_local_environment import NotASandboxLocalEnvironment -from ....storage import Storage -from ....tools._caller import _ToolCaller -from ....tools.executors import ConcurrentToolExecutor -from ....tools.executors._executor import ToolExecutor -from ....tools.registry import ToolRegistry -from ....tools.tool_provider import ToolProvider -from ....tools.watcher import ToolWatcher -from ....types._snapshot import ( +from ...hooks.registry import TEvent +from ...interrupt import _InterruptState +from ...sandbox import Sandbox +from ...sandbox.not_a_sandbox_local_environment import NotASandboxLocalEnvironment +from ...storage import Storage +from ...tools._caller import _ToolCaller +from ...tools.executors import ConcurrentToolExecutor +from ...tools.executors._executor import ToolExecutor +from ...tools.registry import ToolRegistry +from ...tools.tool_provider import ToolProvider +from ...tools.watcher import ToolWatcher +from ...types._snapshot import ( BIDI_SNAPSHOT_FIELDS, BIDI_SNAPSHOT_PRESETS, SNAPSHOT_SCHEMA_VERSION, @@ -53,8 +53,8 @@ from ....types._snapshot import ( SnapshotPreset, resolve_snapshot_fields, ) -from ....types.agent import LocalAgent -from ....types.content import ( +from ...types.agent import LocalAgent +from ...types.content import ( Message, Messages, SystemContentBlock, @@ -62,9 +62,9 @@ from ....types.content import ( _ensure_tracking_id, split_system_prompt, ) -from ....types.exceptions import SnapshotException -from ....types.media import AudioContent, ImageBlock, ImageContent -from ....types.tools import AgentTool +from ...types.exceptions import SnapshotException +from ...types.media import AudioContent, ImageBlock, ImageContent +from ...types.tools import AgentTool from .._async import _TaskGroup, stop_all from ..models.model import BidiModel from ..types.agent import BidiAgentInput @@ -75,8 +75,8 @@ from ..types.media import AudioDelta from .loop import _AgentLoop if TYPE_CHECKING: - from ...._context_manager.context_manager import ContextManager - from ....telemetry.metrics import EventLoopMetrics + from ..._context_manager.context_manager import ContextManager + from ...telemetry.metrics import EventLoopMetrics logger = logging.getLogger(__name__) diff --git a/strands-py/src/strands/experimental/bidi/agent/loop.py b/strands-py/src/strands/bidi/agent/loop.py similarity index 99% rename from strands-py/src/strands/experimental/bidi/agent/loop.py rename to strands-py/src/strands/bidi/agent/loop.py index 21dde3603..bbde416eb 100644 --- a/strands-py/src/strands/experimental/bidi/agent/loop.py +++ b/strands-py/src/strands/bidi/agent/loop.py @@ -12,10 +12,10 @@ from typing import TYPE_CHECKING, Any, Literal, cast from opentelemetry.trace import Span -from ....telemetry.tracer import get_tracer -from ....types._events import ToolInterruptEvent, ToolResultEvent, ToolResultMessageEvent -from ....types.content import ContentBlock, Message -from ....types.tools import ToolResult, ToolResultBlock, ToolUse +from ...telemetry.tracer import get_tracer +from ...types._events import ToolInterruptEvent, ToolResultEvent, ToolResultMessageEvent +from ...types.content import ContentBlock, Message +from ...types.tools import ToolResult, ToolResultBlock, ToolUse from .. import _telemetry from .._async import _TaskGroup, _TaskPool, stop_all from ..hooks.events import ( diff --git a/strands-py/src/strands/bidi/hooks/__init__.py b/strands-py/src/strands/bidi/hooks/__init__.py new file mode 100644 index 000000000..bd923bcbc --- /dev/null +++ b/strands-py/src/strands/bidi/hooks/__init__.py @@ -0,0 +1,17 @@ +"""Hook events for bidirectional agents.""" + +from .events import ( + BidiAfterConnectionRestartEvent, + BidiAgentStopEvent, + BidiBargeInEvent, + BidiBeforeConnectionRestartEvent, + BidiResponseStopEvent, +) + +__all__ = [ + "BidiAgentStopEvent", + "BidiResponseStopEvent", + "BidiBargeInEvent", + "BidiBeforeConnectionRestartEvent", + "BidiAfterConnectionRestartEvent", +] diff --git a/strands-py/src/strands/experimental/bidi/hooks/events.py b/strands-py/src/strands/bidi/hooks/events.py similarity index 98% rename from strands-py/src/strands/experimental/bidi/hooks/events.py rename to strands-py/src/strands/bidi/hooks/events.py index 870a1648f..adecbb2d8 100644 --- a/strands-py/src/strands/experimental/bidi/hooks/events.py +++ b/strands-py/src/strands/bidi/hooks/events.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Literal -from ....hooks.registry import BaseHookEvent +from ...hooks.registry import BaseHookEvent if TYPE_CHECKING: from ..agent.agent import BidiAgent diff --git a/strands-py/src/strands/bidi/io/__init__.py b/strands-py/src/strands/bidi/io/__init__.py new file mode 100644 index 000000000..491676c4b --- /dev/null +++ b/strands-py/src/strands/bidi/io/__init__.py @@ -0,0 +1,24 @@ +"""IO channel implementations for bidirectional streaming.""" + +from typing import TYPE_CHECKING, Any + +from .configs import AudioIOConfig, AudioProcessorConfig, ConsoleIOConfig + +if TYPE_CHECKING: + from .audio import AudioIO + from .console import ConsoleIO + +__all__ = ["AudioIO", "AudioIOConfig", "AudioProcessorConfig", "ConsoleIO", "ConsoleIOConfig"] + + +def __getattr__(name: str) -> Any: + """Lazy load optional I/O implementations only when accessed.""" + if name == "AudioIO": + from .audio import AudioIO + + return AudioIO + if name == "ConsoleIO": + from .console import ConsoleIO + + return ConsoleIO + raise AttributeError(f"cannot import name '{name}' from '{__name__}' ({__file__})") diff --git a/strands-py/src/strands/experimental/bidi/io/audio.py b/strands-py/src/strands/bidi/io/audio.py similarity index 99% rename from strands-py/src/strands/experimental/bidi/io/audio.py rename to strands-py/src/strands/bidi/io/audio.py index 626c6642c..dfcaead7d 100644 --- a/strands-py/src/strands/experimental/bidi/io/audio.py +++ b/strands-py/src/strands/bidi/io/audio.py @@ -322,7 +322,7 @@ class AudioIO: Example: ```python - from strands.experimental.bidi.io import AudioIO, AudioProcessorConfig + from strands.bidi.io import AudioIO, AudioProcessorConfig # Plain mic/speaker, no processing (a headset is recommended to avoid echo): audio_io = AudioIO() diff --git a/strands-py/src/strands/experimental/bidi/io/configs.py b/strands-py/src/strands/bidi/io/configs.py similarity index 100% rename from strands-py/src/strands/experimental/bidi/io/configs.py rename to strands-py/src/strands/bidi/io/configs.py diff --git a/strands-py/src/strands/experimental/bidi/io/console/__init__.py b/strands-py/src/strands/bidi/io/console/__init__.py similarity index 100% rename from strands-py/src/strands/experimental/bidi/io/console/__init__.py rename to strands-py/src/strands/bidi/io/console/__init__.py diff --git a/strands-py/src/strands/experimental/bidi/io/console/_display.py b/strands-py/src/strands/bidi/io/console/_display.py similarity index 100% rename from strands-py/src/strands/experimental/bidi/io/console/_display.py rename to strands-py/src/strands/bidi/io/console/_display.py diff --git a/strands-py/src/strands/experimental/bidi/io/console/_io.py b/strands-py/src/strands/bidi/io/console/_io.py similarity index 99% rename from strands-py/src/strands/experimental/bidi/io/console/_io.py rename to strands-py/src/strands/bidi/io/console/_io.py index 211b5ee2f..c39c36993 100644 --- a/strands-py/src/strands/experimental/bidi/io/console/_io.py +++ b/strands-py/src/strands/bidi/io/console/_io.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING from typing_extensions import Unpack -from .....types.content import TextBlock +from ....types.content import TextBlock from ...types.events import ( BidiConnectionRestartEvent, BidiConnectionStopEvent, diff --git a/strands-py/src/strands/experimental/bidi/io/console/_keyboard.py b/strands-py/src/strands/bidi/io/console/_keyboard.py similarity index 100% rename from strands-py/src/strands/experimental/bidi/io/console/_keyboard.py rename to strands-py/src/strands/bidi/io/console/_keyboard.py diff --git a/strands-py/src/strands/bidi/models/__init__.py b/strands-py/src/strands/bidi/models/__init__.py new file mode 100644 index 000000000..49dafac8d --- /dev/null +++ b/strands-py/src/strands/bidi/models/__init__.py @@ -0,0 +1,57 @@ +"""Bidirectional model interfaces and implementations.""" + +from typing import TYPE_CHECKING, Any + +from .configs import ( + AudioConfig, + AudioStreamConfig, + BedrockNovaSonicAudioConfig, + BedrockNovaSonicAudioStreamConfig, + ConnectionConfig, + GoogleGeminiLiveAudioConfig, + GoogleGeminiLiveAudioStreamConfig, + ModelConfig, + ModelUpdateConfig, +) +from .model import AudioCapable, BidiModel, ConnectionTimeoutError, Restartable + +if TYPE_CHECKING: + from .bedrock import BedrockNovaSonicModel as BedrockNovaSonicModel + from .google import GoogleGeminiLiveModel as GoogleGeminiLiveModel + from .openai import OpenAIRealtimeModel as OpenAIRealtimeModel + +__all__ = [ + "AudioCapable", + "AudioConfig", + "AudioStreamConfig", + "BedrockNovaSonicAudioConfig", + "BedrockNovaSonicAudioStreamConfig", + "BidiModel", + "ConnectionConfig", + "ConnectionTimeoutError", + "GoogleGeminiLiveAudioConfig", + "GoogleGeminiLiveAudioStreamConfig", + "ModelConfig", + "ModelUpdateConfig", + "Restartable", +] + + +def __getattr__(name: str) -> Any: + """Lazy load bidi model implementations only when accessed. + + This defers the import of optional dependencies until actually needed. + """ + if name == "BedrockNovaSonicModel": + from .bedrock import BedrockNovaSonicModel + + return BedrockNovaSonicModel + if name == "GoogleGeminiLiveModel": + from .google import GoogleGeminiLiveModel + + return GoogleGeminiLiveModel + if name == "OpenAIRealtimeModel": + from .openai import OpenAIRealtimeModel + + return OpenAIRealtimeModel + raise AttributeError(f"cannot import name '{name}' from '{__name__}' ({__file__})") diff --git a/strands-py/src/strands/experimental/bidi/models/bedrock.py b/strands-py/src/strands/bidi/models/bedrock.py similarity index 99% rename from strands-py/src/strands/experimental/bidi/models/bedrock.py rename to strands-py/src/strands/bidi/models/bedrock.py index d0b91a9bd..460eb8a4e 100644 --- a/strands-py/src/strands/experimental/bidi/models/bedrock.py +++ b/strands-py/src/strands/bidi/models/bedrock.py @@ -46,9 +46,9 @@ from smithy_core.shapes import ShapeID from smithy_http.aio.crt import AWSCRTHTTPClient, AWSCRTHTTPResponse from typing_extensions import Unpack, override -from ....models._validation import validate_config_keys, validate_region -from ....types.content import Messages, TextBlock -from ....types.tools import ToolResultBlock, ToolSpec, ToolUse +from ...models._validation import validate_config_keys, validate_region +from ...types.content import Messages, TextBlock +from ...types.tools import ToolResultBlock, ToolSpec, ToolUse from .._async import stop_all from ..types.content import BidiContentDelta, BidiMessage from ..types.events import ( diff --git a/strands-py/src/strands/experimental/bidi/models/configs.py b/strands-py/src/strands/bidi/models/configs.py similarity index 98% rename from strands-py/src/strands/experimental/bidi/models/configs.py rename to strands-py/src/strands/bidi/models/configs.py index db5dc4f78..e67062b27 100644 --- a/strands-py/src/strands/experimental/bidi/models/configs.py +++ b/strands-py/src/strands/bidi/models/configs.py @@ -6,7 +6,7 @@ from typing import Any, Literal from typing_extensions import Required, TypedDict -from ....models._validation import validate_config_keys +from ...models._validation import validate_config_keys from ..types.events import AudioChannel, AudioFormat diff --git a/strands-py/src/strands/experimental/bidi/models/google.py b/strands-py/src/strands/bidi/models/google.py similarity index 99% rename from strands-py/src/strands/experimental/bidi/models/google.py rename to strands-py/src/strands/bidi/models/google.py index 67611a398..f0248e380 100644 --- a/strands-py/src/strands/experimental/bidi/models/google.py +++ b/strands-py/src/strands/bidi/models/google.py @@ -24,10 +24,10 @@ from google.genai import types as genai_types from google.genai.types import LiveConnectConfigOrDict, LiveServerContent, LiveServerMessage, UsageMetadata from typing_extensions import Unpack, override -from ....models._validation import validate_config_keys -from ....types.content import Messages, TextBlock -from ....types.media import ImageBlock -from ....types.tools import ToolResultBlock, ToolSpec, ToolUse +from ...models._validation import validate_config_keys +from ...types.content import Messages, TextBlock +from ...types.media import ImageBlock +from ...types.tools import ToolResultBlock, ToolSpec, ToolUse from .._async import stop_all from ..types.content import BidiContentDelta, BidiMessage from ..types.events import ( diff --git a/strands-py/src/strands/experimental/bidi/models/model.py b/strands-py/src/strands/bidi/models/model.py similarity index 97% rename from strands-py/src/strands/experimental/bidi/models/model.py rename to strands-py/src/strands/bidi/models/model.py index eaf01d6e5..e7a4265a8 100644 --- a/strands-py/src/strands/experimental/bidi/models/model.py +++ b/strands-py/src/strands/bidi/models/model.py @@ -18,9 +18,9 @@ import logging from collections.abc import AsyncIterable from typing import Any, NoReturn, Protocol, cast, runtime_checkable -from ....models.model import Model -from ....types.content import Messages -from ....types.tools import ToolSpec +from ...models.model import Model +from ...types.content import Messages +from ...types.tools import ToolSpec from ..types.content import BidiContentDelta, BidiMessage from ..types.events import BidiOutputEvent from .configs import AudioConfig, ConnectionConfig @@ -150,7 +150,7 @@ class BidiModel(Model, abc.ABC): Example: ``` - from strands.experimental.bidi.types import AudioDelta, BidiMessage + from strands.bidi.types import AudioDelta, BidiMessage from strands.types.content import TextBlock from strands.types.media import ImageBlock from strands.types.tools import ToolResultBlock diff --git a/strands-py/src/strands/experimental/bidi/models/openai.py b/strands-py/src/strands/bidi/models/openai.py similarity index 99% rename from strands-py/src/strands/experimental/bidi/models/openai.py rename to strands-py/src/strands/bidi/models/openai.py index 6db78e7d1..4ef5a0c2c 100644 --- a/strands-py/src/strands/experimental/bidi/models/openai.py +++ b/strands-py/src/strands/bidi/models/openai.py @@ -20,9 +20,9 @@ import websockets from typing_extensions import Unpack, override from websockets import ClientConnection -from ....types.content import Messages, TextBlock -from ....types.media import ImageBlock -from ....types.tools import ToolResultBlock, ToolSpec, ToolUse +from ...types.content import Messages, TextBlock +from ...types.media import ImageBlock +from ...types.tools import ToolResultBlock, ToolSpec, ToolUse from .._async import stop_all from ..types.content import BidiContentDelta, BidiMessage from ..types.events import ( diff --git a/strands-py/src/strands/bidi/types/__init__.py b/strands-py/src/strands/bidi/types/__init__.py new file mode 100644 index 000000000..953705fcc --- /dev/null +++ b/strands-py/src/strands/bidi/types/__init__.py @@ -0,0 +1,90 @@ +"""Type definitions for bidirectional streaming.""" + +from .agent import BidiAgentInput +from .content import ( + BidiContentBlock, + BidiContentBlockData, + BidiContentDelta, + BidiContentDeltaData, + BidiContentMetadata, + BidiMessage, + BidiUserContentBlock, + BidiUserContentBlockData, +) +from .events import ( + AudioChannel, + AudioFormat, + BidiAudioDeltaEvent, + BidiAudioStartEvent, + BidiAudioStopEvent, + BidiBargeInEvent, + BidiConnectionRestartEvent, + BidiConnectionStartEvent, + BidiConnectionStopEvent, + BidiConnectionWarningEvent, + BidiOutputEvent, + BidiReasoningBlockEvent, + BidiReasoningDeltaEvent, + BidiReasoningStartEvent, + BidiReasoningStopEvent, + BidiResponseStartEvent, + BidiResponseStopEvent, + BidiTextBlockEvent, + BidiTextDeltaEvent, + BidiTextStartEvent, + BidiTextStopEvent, + BidiToolUseBlocksEvent, + BidiTranscriptBlockEvent, + BidiTranscriptDeltaEvent, + BidiTranscriptStartEvent, + BidiTranscriptStopEvent, + BidiUsageEvent, + ModalityUsage, + Role, +) +from .io import InputStream, OutputStream +from .media import AudioDelta + +__all__ = [ + "AudioChannel", + "AudioDelta", + "AudioFormat", + "BidiAgentInput", + "BidiAudioDeltaEvent", + "BidiAudioStartEvent", + "BidiAudioStopEvent", + "BidiContentBlock", + "BidiContentBlockData", + "BidiContentDelta", + "BidiContentDeltaData", + "BidiContentMetadata", + "BidiConnectionRestartEvent", + "BidiConnectionStartEvent", + "BidiConnectionStopEvent", + "BidiConnectionWarningEvent", + "BidiMessage", + "BidiOutputEvent", + "BidiReasoningBlockEvent", + "BidiReasoningDeltaEvent", + "BidiReasoningStartEvent", + "BidiReasoningStopEvent", + "BidiBargeInEvent", + "BidiResponseStartEvent", + "BidiResponseStopEvent", + "BidiToolUseBlocksEvent", + "BidiTextBlockEvent", + "BidiTextDeltaEvent", + "BidiTextStartEvent", + "BidiTextStopEvent", + "BidiTranscriptDeltaEvent", + "BidiTranscriptStartEvent", + "BidiTranscriptStopEvent", + "BidiTranscriptBlockEvent", + "BidiUsageEvent", + "BidiUserContentBlock", + "BidiUserContentBlockData", + "InputStream", + "ModalityUsage", + "OutputStream", + "Role", +] diff --git a/strands-py/src/strands/experimental/bidi/types/agent.py b/strands-py/src/strands/bidi/types/agent.py similarity index 100% rename from strands-py/src/strands/experimental/bidi/types/agent.py rename to strands-py/src/strands/bidi/types/agent.py diff --git a/strands-py/src/strands/experimental/bidi/types/content.py b/strands-py/src/strands/bidi/types/content.py similarity index 91% rename from strands-py/src/strands/experimental/bidi/types/content.py rename to strands-py/src/strands/bidi/types/content.py index cc2cf0400..5956bb1d1 100644 --- a/strands-py/src/strands/experimental/bidi/types/content.py +++ b/strands-py/src/strands/bidi/types/content.py @@ -5,9 +5,9 @@ from typing import Literal, TypeAlias from typing_extensions import TypedDict -from ....types.content import TextBlock, _TextBlockData -from ....types.media import ImageBlock, _ImageBlockData -from ....types.tools import ToolResultBlock, _ToolResultBlockData +from ...types.content import TextBlock, _TextBlockData +from ...types.media import ImageBlock, _ImageBlockData +from ...types.tools import ToolResultBlock, _ToolResultBlockData from .media import AudioDelta, _AudioDeltaData BidiUserContentBlock: TypeAlias = TextBlock | ImageBlock diff --git a/strands-py/src/strands/experimental/bidi/types/events.py b/strands-py/src/strands/bidi/types/events.py similarity index 99% rename from strands-py/src/strands/experimental/bidi/types/events.py rename to strands-py/src/strands/bidi/types/events.py index 3b4e152e5..40fa5c9ec 100644 --- a/strands-py/src/strands/experimental/bidi/types/events.py +++ b/strands-py/src/strands/bidi/types/events.py @@ -24,8 +24,8 @@ Audio format normalization: import logging from typing import TYPE_CHECKING, Any, Literal, cast, get_args -from ....types._events import TypedEvent -from ....types.tools import ToolUse +from ...types._events import TypedEvent +from ...types.tools import ToolUse if TYPE_CHECKING: from ..models.model import ConnectionTimeoutError diff --git a/strands-py/src/strands/experimental/bidi/types/io.py b/strands-py/src/strands/bidi/types/io.py similarity index 100% rename from strands-py/src/strands/experimental/bidi/types/io.py rename to strands-py/src/strands/bidi/types/io.py diff --git a/strands-py/src/strands/experimental/bidi/types/media.py b/strands-py/src/strands/bidi/types/media.py similarity index 91% rename from strands-py/src/strands/experimental/bidi/types/media.py rename to strands-py/src/strands/bidi/types/media.py index 47ce71182..475017b1a 100644 --- a/strands-py/src/strands/experimental/bidi/types/media.py +++ b/strands-py/src/strands/bidi/types/media.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from typing_extensions import TypedDict -from ....types.media import AudioContent, AudioFormat, AudioSource +from ...types.media import AudioContent, AudioFormat, AudioSource class _AudioDeltaData(TypedDict): diff --git a/strands-py/src/strands/experimental/bidi/__init__.py b/strands-py/src/strands/experimental/bidi/__init__.py index 62c6134a4..2dd1a5b92 100644 --- a/strands-py/src/strands/experimental/bidi/__init__.py +++ b/strands-py/src/strands/experimental/bidi/__init__.py @@ -1,7 +1,60 @@ -"""Experimental bidirectional streaming APIs.""" +"""Deprecated alias for :mod:`strands.bidi`.""" + +import importlib +import importlib.abc +import importlib.machinery +import importlib.util +import sys +import warnings +from collections.abc import Sequence +from types import ModuleType + +from strands.bidi import BidiAgent as BidiAgent from . import agent, hooks, io, models, types -from .agent import BidiAgent as BidiAgent -# Compatibility for AgentCore's root import; remove after AgentCore imports from bidi.agent. __all__ = ["agent", "hooks", "io", "models", "types"] + +_DEPRECATED_PACKAGE = __name__ +_STABLE_PACKAGE = "strands.bidi" +# Subpackages that keep a shim file; every other submodule resolves through _StableModuleFinder. +_SHIM_PACKAGES = frozenset(f"{_DEPRECATED_PACKAGE}.{name}" for name in ("agent", "hooks", "io", "models", "types")) + + +def _stable_name(deprecated_name: str) -> str: + return _STABLE_PACKAGE + deprecated_name.removeprefix(_DEPRECATED_PACKAGE) + + +class _StableModuleFinder(importlib.abc.MetaPathFinder, importlib.abc.Loader): + """Import deprecated submodule paths as the stable module object, so identity, patching, and pickling hold.""" + + def find_spec( + self, + fullname: str, + path: Sequence[str] | None, + target: ModuleType | None = None, + ) -> importlib.machinery.ModuleSpec | None: + """Claim deprecated submodules that have a stable counterpart and no shim file.""" + if not fullname.startswith(f"{_DEPRECATED_PACKAGE}.") or fullname in _SHIM_PACKAGES: + return None + if importlib.util.find_spec(_stable_name(fullname)) is None: + return None + return importlib.machinery.ModuleSpec(fullname, self) + + def create_module(self, spec: importlib.machinery.ModuleSpec) -> ModuleType | None: + """Use the default placeholder module, which exec_module replaces.""" + return None + + def exec_module(self, module: ModuleType) -> None: + """Replace the placeholder with the stable module.""" + sys.modules[module.__name__] = importlib.import_module(_stable_name(module.__name__)) + + +# Ahead of the path finder, which would otherwise load a second copy of modules under aliased stable packages. +sys.meta_path.insert(0, _StableModuleFinder()) + +warnings.warn( + "strands.experimental.bidi is deprecated and will be removed in v1.60.0. Import from strands.bidi instead.", + DeprecationWarning, + stacklevel=2, +) diff --git a/strands-py/src/strands/experimental/bidi/agent/__init__.py b/strands-py/src/strands/experimental/bidi/agent/__init__.py index 564973099..588b33a39 100644 --- a/strands-py/src/strands/experimental/bidi/agent/__init__.py +++ b/strands-py/src/strands/experimental/bidi/agent/__init__.py @@ -1,5 +1,13 @@ -"""Bidirectional agent for real-time streaming conversations.""" +"""Deprecated alias for :mod:`strands.bidi.agent`.""" -from .agent import BidiAgent +from typing import Any + +from strands.bidi import agent as _real +from strands.bidi.agent import BidiAgent __all__ = ["BidiAgent"] + + +def __getattr__(name: str) -> Any: + """Forward attribute access to :mod:`strands.bidi.agent`.""" + return getattr(_real, name) diff --git a/strands-py/src/strands/experimental/bidi/hooks/__init__.py b/strands-py/src/strands/experimental/bidi/hooks/__init__.py index bd923bcbc..dce72279d 100644 --- a/strands-py/src/strands/experimental/bidi/hooks/__init__.py +++ b/strands-py/src/strands/experimental/bidi/hooks/__init__.py @@ -1,6 +1,9 @@ -"""Hook events for bidirectional agents.""" +"""Deprecated alias for :mod:`strands.bidi.hooks`.""" -from .events import ( +from typing import Any + +from strands.bidi import hooks as _real +from strands.bidi.hooks import ( BidiAfterConnectionRestartEvent, BidiAgentStopEvent, BidiBargeInEvent, @@ -15,3 +18,8 @@ __all__ = [ "BidiBeforeConnectionRestartEvent", "BidiAfterConnectionRestartEvent", ] + + +def __getattr__(name: str) -> Any: + """Forward attribute access to :mod:`strands.bidi.hooks`.""" + return getattr(_real, name) diff --git a/strands-py/src/strands/experimental/bidi/io/__init__.py b/strands-py/src/strands/experimental/bidi/io/__init__.py index 491676c4b..056ed8737 100644 --- a/strands-py/src/strands/experimental/bidi/io/__init__.py +++ b/strands-py/src/strands/experimental/bidi/io/__init__.py @@ -1,24 +1,16 @@ -"""IO channel implementations for bidirectional streaming.""" +"""Deprecated alias for :mod:`strands.bidi.io`.""" from typing import TYPE_CHECKING, Any -from .configs import AudioIOConfig, AudioProcessorConfig, ConsoleIOConfig +from strands.bidi import io as _real +from strands.bidi.io import AudioIOConfig, AudioProcessorConfig, ConsoleIOConfig if TYPE_CHECKING: - from .audio import AudioIO - from .console import ConsoleIO + from strands.bidi.io import AudioIO, ConsoleIO __all__ = ["AudioIO", "AudioIOConfig", "AudioProcessorConfig", "ConsoleIO", "ConsoleIOConfig"] def __getattr__(name: str) -> Any: - """Lazy load optional I/O implementations only when accessed.""" - if name == "AudioIO": - from .audio import AudioIO - - return AudioIO - if name == "ConsoleIO": - from .console import ConsoleIO - - return ConsoleIO - raise AttributeError(f"cannot import name '{name}' from '{__name__}' ({__file__})") + """Forward attribute access to :mod:`strands.bidi.io`.""" + return getattr(_real, name) diff --git a/strands-py/src/strands/experimental/bidi/models/__init__.py b/strands-py/src/strands/experimental/bidi/models/__init__.py index 49dafac8d..9bb270025 100644 --- a/strands-py/src/strands/experimental/bidi/models/__init__.py +++ b/strands-py/src/strands/experimental/bidi/models/__init__.py @@ -1,24 +1,28 @@ -"""Bidirectional model interfaces and implementations.""" +"""Deprecated alias for :mod:`strands.bidi.models`.""" from typing import TYPE_CHECKING, Any -from .configs import ( +from strands.bidi import models as _real +from strands.bidi.models import ( + AudioCapable, AudioConfig, AudioStreamConfig, BedrockNovaSonicAudioConfig, BedrockNovaSonicAudioStreamConfig, + BidiModel, ConnectionConfig, + ConnectionTimeoutError, GoogleGeminiLiveAudioConfig, GoogleGeminiLiveAudioStreamConfig, ModelConfig, ModelUpdateConfig, + Restartable, ) -from .model import AudioCapable, BidiModel, ConnectionTimeoutError, Restartable if TYPE_CHECKING: - from .bedrock import BedrockNovaSonicModel as BedrockNovaSonicModel - from .google import GoogleGeminiLiveModel as GoogleGeminiLiveModel - from .openai import OpenAIRealtimeModel as OpenAIRealtimeModel + from strands.bidi.models import BedrockNovaSonicModel as BedrockNovaSonicModel + from strands.bidi.models import GoogleGeminiLiveModel as GoogleGeminiLiveModel + from strands.bidi.models import OpenAIRealtimeModel as OpenAIRealtimeModel __all__ = [ "AudioCapable", @@ -38,20 +42,5 @@ __all__ = [ def __getattr__(name: str) -> Any: - """Lazy load bidi model implementations only when accessed. - - This defers the import of optional dependencies until actually needed. - """ - if name == "BedrockNovaSonicModel": - from .bedrock import BedrockNovaSonicModel - - return BedrockNovaSonicModel - if name == "GoogleGeminiLiveModel": - from .google import GoogleGeminiLiveModel - - return GoogleGeminiLiveModel - if name == "OpenAIRealtimeModel": - from .openai import OpenAIRealtimeModel - - return OpenAIRealtimeModel - raise AttributeError(f"cannot import name '{name}' from '{__name__}' ({__file__})") + """Forward attribute access to :mod:`strands.bidi.models`.""" + return getattr(_real, name) diff --git a/strands-py/src/strands/experimental/bidi/types/__init__.py b/strands-py/src/strands/experimental/bidi/types/__init__.py index 953705fcc..700ce26f8 100644 --- a/strands-py/src/strands/experimental/bidi/types/__init__.py +++ b/strands-py/src/strands/experimental/bidi/types/__init__.py @@ -1,19 +1,13 @@ -"""Type definitions for bidirectional streaming.""" +"""Deprecated alias for :mod:`strands.bidi.types`.""" -from .agent import BidiAgentInput -from .content import ( - BidiContentBlock, - BidiContentBlockData, - BidiContentDelta, - BidiContentDeltaData, - BidiContentMetadata, - BidiMessage, - BidiUserContentBlock, - BidiUserContentBlockData, -) -from .events import ( +from typing import Any + +from strands.bidi import types as _real +from strands.bidi.types import ( AudioChannel, + AudioDelta, AudioFormat, + BidiAgentInput, BidiAudioDeltaEvent, BidiAudioStartEvent, BidiAudioStopEvent, @@ -22,6 +16,12 @@ from .events import ( BidiConnectionStartEvent, BidiConnectionStopEvent, BidiConnectionWarningEvent, + BidiContentBlock, + BidiContentBlockData, + BidiContentDelta, + BidiContentDeltaData, + BidiContentMetadata, + BidiMessage, BidiOutputEvent, BidiReasoningBlockEvent, BidiReasoningDeltaEvent, @@ -39,11 +39,13 @@ from .events import ( BidiTranscriptStartEvent, BidiTranscriptStopEvent, BidiUsageEvent, + BidiUserContentBlock, + BidiUserContentBlockData, + InputStream, ModalityUsage, + OutputStream, Role, ) -from .io import InputStream, OutputStream -from .media import AudioDelta __all__ = [ "AudioChannel", @@ -88,3 +90,8 @@ __all__ = [ "OutputStream", "Role", ] + + +def __getattr__(name: str) -> Any: + """Forward attribute access to :mod:`strands.bidi.types`.""" + return getattr(_real, name) diff --git a/strands-py/src/strands/session/snapshot_session_manager.py b/strands-py/src/strands/session/snapshot_session_manager.py index d94da0dc6..a720d3ef8 100644 --- a/strands-py/src/strands/session/snapshot_session_manager.py +++ b/strands-py/src/strands/session/snapshot_session_manager.py @@ -49,7 +49,7 @@ from .session_manager import SessionManager if TYPE_CHECKING: from .._context_manager.stash import Stash from ..agent.agent import Agent - from ..experimental.bidi.agent import BidiAgent + from ..bidi.agent import BidiAgent from ..multiagent.base import MultiAgentBase logger = logging.getLogger(__name__) @@ -369,7 +369,7 @@ class SnapshotSessionManager(SessionManager): Raises: NotImplementedError: If agent is a BidiAgent. """ - from ..experimental.bidi.agent import BidiAgent + from ..bidi.agent import BidiAgent if isinstance(agent, BidiAgent): raise NotImplementedError(f"{type(self).__name__} does not support BidiAgent persistence.") diff --git a/strands-py/src/strands/tools/_caller.py b/strands-py/src/strands/tools/_caller.py index 89f3bfd54..3c561e389 100644 --- a/strands-py/src/strands/tools/_caller.py +++ b/strands-py/src/strands/tools/_caller.py @@ -22,7 +22,7 @@ from ..types.tools import ToolResult, ToolUse if TYPE_CHECKING: from ..agent import Agent - from ..experimental.bidi.agent import BidiAgent + from ..bidi.agent import BidiAgent class _ToolCaller: diff --git a/strands-py/src/strands/tools/executors/_executor.py b/strands-py/src/strands/tools/executors/_executor.py index 16008258d..6d7128e1e 100644 --- a/strands-py/src/strands/tools/executors/_executor.py +++ b/strands-py/src/strands/tools/executors/_executor.py @@ -28,7 +28,7 @@ if TYPE_CHECKING: # pragma: no cover from ...agent import Agent from ...background_tasks._background_tasks import _BackgroundTasks from ...background_tasks.in_process._manager import _MiddlewareInterrupt - from ...experimental.bidi.agent import BidiAgent + from ...bidi.agent import BidiAgent logger = logging.getLogger(__name__) diff --git a/strands-py/tests/strands/agent/test_agent_storage.py b/strands-py/tests/strands/agent/test_agent_storage.py index 9b90ba46d..be6f7649b 100644 --- a/strands-py/tests/strands/agent/test_agent_storage.py +++ b/strands-py/tests/strands/agent/test_agent_storage.py @@ -6,8 +6,8 @@ from unittest.mock import MagicMock import pytest from strands import Agent -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.models import BidiModel +from strands.bidi.agent import BidiAgent +from strands.bidi.models import BidiModel from strands.session.repository_session_manager import RepositorySessionManager from strands.session.snapshot_session_manager import SnapshotSessionManager from strands.storage.in_memory_storage import InMemoryStorage as UnifiedInMemoryStorage diff --git a/strands-py/tests/strands/bidi/__init__.py b/strands-py/tests/strands/bidi/__init__.py new file mode 100644 index 000000000..153123e7a --- /dev/null +++ b/strands-py/tests/strands/bidi/__init__.py @@ -0,0 +1 @@ +"""Tests for bidirectional streaming.""" diff --git a/strands-py/tests/strands/experimental/bidi/_async/__init__.py b/strands-py/tests/strands/bidi/_async/__init__.py similarity index 100% rename from strands-py/tests/strands/experimental/bidi/_async/__init__.py rename to strands-py/tests/strands/bidi/_async/__init__.py diff --git a/strands-py/tests/strands/experimental/bidi/_async/test__init__.py b/strands-py/tests/strands/bidi/_async/test__init__.py similarity index 94% rename from strands-py/tests/strands/experimental/bidi/_async/test__init__.py rename to strands-py/tests/strands/bidi/_async/test__init__.py index a121ddecc..a9f9a3226 100644 --- a/strands-py/tests/strands/experimental/bidi/_async/test__init__.py +++ b/strands-py/tests/strands/bidi/_async/test__init__.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock import pytest -from strands.experimental.bidi._async import stop_all +from strands.bidi._async import stop_all @pytest.mark.asyncio diff --git a/strands-py/tests/strands/experimental/bidi/_async/test_task_group.py b/strands-py/tests/strands/bidi/_async/test_task_group.py similarity index 96% rename from strands-py/tests/strands/experimental/bidi/_async/test_task_group.py rename to strands-py/tests/strands/bidi/_async/test_task_group.py index 255ead15e..a3cc24bee 100644 --- a/strands-py/tests/strands/experimental/bidi/_async/test_task_group.py +++ b/strands-py/tests/strands/bidi/_async/test_task_group.py @@ -3,7 +3,7 @@ import unittest.mock import pytest -from strands.experimental.bidi._async._task_group import _TaskGroup +from strands.bidi._async._task_group import _TaskGroup @pytest.mark.asyncio diff --git a/strands-py/tests/strands/experimental/bidi/_async/test_task_pool.py b/strands-py/tests/strands/bidi/_async/test_task_pool.py similarity index 93% rename from strands-py/tests/strands/experimental/bidi/_async/test_task_pool.py rename to strands-py/tests/strands/bidi/_async/test_task_pool.py index 35f817954..490480b7f 100644 --- a/strands-py/tests/strands/experimental/bidi/_async/test_task_pool.py +++ b/strands-py/tests/strands/bidi/_async/test_task_pool.py @@ -2,7 +2,7 @@ import asyncio import pytest -from strands.experimental.bidi._async._task_pool import _TaskPool +from strands.bidi._async._task_pool import _TaskPool @pytest.fixture diff --git a/strands-py/tests/strands/experimental/bidi/_audio/test_buffer.py b/strands-py/tests/strands/bidi/_audio/test_buffer.py similarity index 95% rename from strands-py/tests/strands/experimental/bidi/_audio/test_buffer.py rename to strands-py/tests/strands/bidi/_audio/test_buffer.py index d81085e95..5eee698d5 100644 --- a/strands-py/tests/strands/experimental/bidi/_audio/test_buffer.py +++ b/strands-py/tests/strands/bidi/_audio/test_buffer.py @@ -1,6 +1,6 @@ import pytest -from strands.experimental.bidi._audio.buffer import AudioBuffer +from strands.bidi._audio.buffer import AudioBuffer @pytest.fixture diff --git a/strands-py/tests/strands/experimental/bidi/_audio/test_processor.py b/strands-py/tests/strands/bidi/_audio/test_processor.py similarity index 99% rename from strands-py/tests/strands/experimental/bidi/_audio/test_processor.py rename to strands-py/tests/strands/bidi/_audio/test_processor.py index e11aa3f60..41f525d82 100644 --- a/strands-py/tests/strands/experimental/bidi/_audio/test_processor.py +++ b/strands-py/tests/strands/bidi/_audio/test_processor.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from strands.experimental.bidi._audio.processor import AudioProcessor +from strands.bidi._audio.processor import AudioProcessor def _create_processor( diff --git a/strands-py/tests/strands/experimental/bidi/agent/__init__.py b/strands-py/tests/strands/bidi/agent/__init__.py similarity index 100% rename from strands-py/tests/strands/experimental/bidi/agent/__init__.py rename to strands-py/tests/strands/bidi/agent/__init__.py diff --git a/strands-py/tests/strands/experimental/bidi/agent/test_agent.py b/strands-py/tests/strands/bidi/agent/test_agent.py similarity index 98% rename from strands-py/tests/strands/experimental/bidi/agent/test_agent.py rename to strands-py/tests/strands/bidi/agent/test_agent.py index 3a4d2d559..cfba4a2ef 100644 --- a/strands-py/tests/strands/experimental/bidi/agent/test_agent.py +++ b/strands-py/tests/strands/bidi/agent/test_agent.py @@ -9,9 +9,9 @@ from uuid import uuid4 import pytest from strands import LocalAgent, ToolContext, tool -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.models import BidiModel -from strands.experimental.bidi.types import ( +from strands.bidi.agent import BidiAgent +from strands.bidi.models import BidiModel +from strands.bidi.types import ( AudioDelta, BidiAudioDeltaEvent, BidiConnectionStartEvent, @@ -119,10 +119,10 @@ def mock_tool_caller(): @pytest.fixture def agent(mock_model, mock_tool_registry, mock_tool_caller): """Create a BidiAgent instance for testing.""" - with unittest.mock.patch("strands.experimental.bidi.agent.agent.ToolRegistry") as mock_registry_class: + with unittest.mock.patch("strands.bidi.agent.agent.ToolRegistry") as mock_registry_class: mock_registry_class.return_value = mock_tool_registry - with unittest.mock.patch("strands.experimental.bidi.agent.agent._ToolCaller") as mock_caller_class: + with unittest.mock.patch("strands.bidi.agent.agent._ToolCaller") as mock_caller_class: mock_caller_class.return_value = mock_tool_caller # Don't pass tools to avoid real tool loading @@ -377,7 +377,7 @@ def test_bidi_agent_event_loop_metrics_setter_raises(mock_model): @pytest.mark.skipif(sys.version_info < (3, 12), reason="BedrockNovaSonicModel is only supported for Python 3.12+") @pytest.mark.parametrize("options", [{}, {"model": None}]) def test_bidi_agent_init_with_default_model(options): - from strands.experimental.bidi.models import BedrockNovaSonicModel + from strands.bidi.models import BedrockNovaSonicModel agent = BidiAgent(**options) @@ -387,7 +387,7 @@ def test_bidi_agent_init_with_default_model(options): @pytest.mark.skipif(sys.version_info < (3, 12), reason="BedrockNovaSonicModel is only supported for Python 3.12+") def test_bidi_agent_init_with_model_id(): - from strands.experimental.bidi.models import BedrockNovaSonicModel + from strands.bidi.models import BedrockNovaSonicModel model_id = "custom-model" agent = BidiAgent(model=model_id) diff --git a/strands-py/tests/strands/experimental/bidi/agent/test_loop.py b/strands-py/tests/strands/bidi/agent/test_loop.py similarity index 99% rename from strands-py/tests/strands/experimental/bidi/agent/test_loop.py rename to strands-py/tests/strands/bidi/agent/test_loop.py index 3acef72c0..e05ac4a35 100644 --- a/strands-py/tests/strands/experimental/bidi/agent/test_loop.py +++ b/strands-py/tests/strands/bidi/agent/test_loop.py @@ -5,13 +5,13 @@ import pytest import pytest_asyncio from strands import LocalAgent, ToolContext, tool -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.agent.loop import _ReaderError -from strands.experimental.bidi.hooks import BidiAgentStopEvent, BidiBeforeConnectionRestartEvent -from strands.experimental.bidi.hooks import BidiBargeInEvent as BidiBargeInHookEvent -from strands.experimental.bidi.hooks import BidiResponseStopEvent as BidiResponseStopHookEvent -from strands.experimental.bidi.models import BidiModel, ConnectionTimeoutError -from strands.experimental.bidi.types import ( +from strands.bidi.agent import BidiAgent +from strands.bidi.agent.loop import _ReaderError +from strands.bidi.hooks import BidiAgentStopEvent, BidiBeforeConnectionRestartEvent +from strands.bidi.hooks import BidiBargeInEvent as BidiBargeInHookEvent +from strands.bidi.hooks import BidiResponseStopEvent as BidiResponseStopHookEvent +from strands.bidi.models import BidiModel, ConnectionTimeoutError +from strands.bidi.types import ( BidiAudioDeltaEvent, BidiBargeInEvent, BidiConnectionRestartEvent, @@ -1585,7 +1585,7 @@ async def test_forced_swap_flags_interrupted_turn(agent, agenerator): loop._update_turn_state() # Force the turn-alignment wait to time out immediately (no wall-clock wait). - with unittest.mock.patch("strands.experimental.bidi.agent.loop._MODEL_RESTART_TURN_TIMEOUT_S", 0): + with unittest.mock.patch("strands.bidi.agent.loop._MODEL_RESTART_TURN_TIMEOUT_S", 0): await loop._on_reconnect_deadline() restart = await loop._event_queue.get() @@ -1625,7 +1625,7 @@ async def test_proactive_reconnect_waits_for_turn_boundary(loop, agent, agenerat @pytest.mark.asyncio async def test_bidi_agent_loop_restart_hook_reports_reason(loop, agent, agenerator): """The reactive path reports reason='timeout' with the error; proactive reports 'scheduled' with None.""" - from strands.experimental.bidi.hooks import BidiBeforeConnectionRestartEvent + from strands.bidi.hooks import BidiBeforeConnectionRestartEvent before_events = [] agent.hooks.add_callback( @@ -1733,7 +1733,7 @@ async def test_bidi_agent_loop_proactive_reconnect_completes_when_reconnect_susp @pytest.mark.asyncio async def test_bidi_agent_loop_cumulative_usage_not_double_counted(loop, agent, agenerator): """Cumulative providers replace running counts rather than summing successive totals.""" - from strands.experimental.bidi.types import BidiUsageEvent + from strands.bidi.types import BidiUsageEvent agent.model.usage_is_cumulative = True events = [ diff --git a/strands-py/tests/strands/experimental/bidi/agent/test_loop_telemetry.py b/strands-py/tests/strands/bidi/agent/test_loop_telemetry.py similarity index 98% rename from strands-py/tests/strands/experimental/bidi/agent/test_loop_telemetry.py rename to strands-py/tests/strands/bidi/agent/test_loop_telemetry.py index 400f5d563..dbf7c5070 100644 --- a/strands-py/tests/strands/experimental/bidi/agent/test_loop_telemetry.py +++ b/strands-py/tests/strands/bidi/agent/test_loop_telemetry.py @@ -19,15 +19,15 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor, SpanExporter, SpanExportResult from opentelemetry.trace import StatusCode -import strands.experimental.bidi._telemetry as _telemetry +import strands.bidi._telemetry as _telemetry from strands import tool -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.hooks import ( +from strands.bidi.agent import BidiAgent +from strands.bidi.hooks import ( BidiAfterConnectionRestartEvent, BidiBeforeConnectionRestartEvent, ) -from strands.experimental.bidi.models import BidiModel, ConnectionTimeoutError -from strands.experimental.bidi.types import ( +from strands.bidi.models import BidiModel, ConnectionTimeoutError +from strands.bidi.types import ( BidiAudioDeltaEvent, BidiBargeInEvent, BidiConnectionStopEvent, diff --git a/strands-py/tests/strands/experimental/bidi/agent/test_reconnect_timer.py b/strands-py/tests/strands/bidi/agent/test_reconnect_timer.py similarity index 97% rename from strands-py/tests/strands/experimental/bidi/agent/test_reconnect_timer.py rename to strands-py/tests/strands/bidi/agent/test_reconnect_timer.py index 7ed465b07..f6b7dbfd6 100644 --- a/strands-py/tests/strands/experimental/bidi/agent/test_reconnect_timer.py +++ b/strands-py/tests/strands/bidi/agent/test_reconnect_timer.py @@ -8,7 +8,7 @@ import asyncio import pytest -from strands.experimental.bidi.agent._reconnect_timer import _ReconnectTimer, resolve_deadline_s +from strands.bidi.agent._reconnect_timer import _ReconnectTimer, resolve_deadline_s # resolve_deadline_s diff --git a/strands-py/tests/strands/experimental/bidi/agent/test_snapshot.py b/strands-py/tests/strands/bidi/agent/test_snapshot.py similarity index 98% rename from strands-py/tests/strands/experimental/bidi/agent/test_snapshot.py rename to strands-py/tests/strands/bidi/agent/test_snapshot.py index 5767dcacf..db34f48e8 100644 --- a/strands-py/tests/strands/experimental/bidi/agent/test_snapshot.py +++ b/strands-py/tests/strands/bidi/agent/test_snapshot.py @@ -5,8 +5,8 @@ import unittest.mock import pytest -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.models import BidiModel +from strands.bidi.agent import BidiAgent +from strands.bidi.models import BidiModel from strands.types._snapshot import BIDI_SNAPSHOT_PRESETS, SNAPSHOT_SCHEMA_VERSION, Snapshot from strands.types.exceptions import SnapshotException diff --git a/strands-py/tests/strands/experimental/bidi/conftest.py b/strands-py/tests/strands/bidi/conftest.py similarity index 56% rename from strands-py/tests/strands/experimental/bidi/conftest.py rename to strands-py/tests/strands/bidi/conftest.py index 990dda244..d1af21f3c 100644 --- a/strands-py/tests/strands/experimental/bidi/conftest.py +++ b/strands-py/tests/strands/bidi/conftest.py @@ -6,7 +6,5 @@ import pytest @pytest.fixture def mock_audio_processor(): """Mock the native audio processor constructor for one test.""" - with unittest.mock.patch( - "strands.experimental.bidi._audio.processor.pywebrtc_audio.AudioProcessor" - ) as processor_class: + with unittest.mock.patch("strands.bidi._audio.processor.pywebrtc_audio.AudioProcessor") as processor_class: yield processor_class diff --git a/strands-py/tests/strands/experimental/bidi/hooks/__init__.py b/strands-py/tests/strands/bidi/hooks/__init__.py similarity index 100% rename from strands-py/tests/strands/experimental/bidi/hooks/__init__.py rename to strands-py/tests/strands/bidi/hooks/__init__.py diff --git a/strands-py/tests/strands/experimental/bidi/hooks/test_events.py b/strands-py/tests/strands/bidi/hooks/test_events.py similarity index 94% rename from strands-py/tests/strands/experimental/bidi/hooks/test_events.py rename to strands-py/tests/strands/bidi/hooks/test_events.py index 040956ece..03c5262a8 100644 --- a/strands-py/tests/strands/experimental/bidi/hooks/test_events.py +++ b/strands-py/tests/strands/bidi/hooks/test_events.py @@ -6,13 +6,13 @@ from unittest.mock import Mock import pytest from strands import LocalAgent -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.hooks import ( +from strands.bidi.agent import BidiAgent +from strands.bidi.hooks import ( BidiAgentStopEvent, BidiBargeInEvent, BidiResponseStopEvent, ) -from strands.experimental.bidi.models import BidiModel +from strands.bidi.models import BidiModel from strands.hooks import AgentInitializedEvent, MessageAddedEvent from tests.fixtures.mock_hook_provider import MockHookProvider diff --git a/strands-py/tests/strands/experimental/bidi/io/__init__.py b/strands-py/tests/strands/bidi/io/__init__.py similarity index 100% rename from strands-py/tests/strands/experimental/bidi/io/__init__.py rename to strands-py/tests/strands/bidi/io/__init__.py diff --git a/strands-py/tests/strands/experimental/bidi/io/conftest.py b/strands-py/tests/strands/bidi/io/conftest.py similarity index 100% rename from strands-py/tests/strands/experimental/bidi/io/conftest.py rename to strands-py/tests/strands/bidi/io/conftest.py diff --git a/strands-py/tests/strands/experimental/bidi/io/console/test_io.py b/strands-py/tests/strands/bidi/io/console/test_io.py similarity index 98% rename from strands-py/tests/strands/experimental/bidi/io/console/test_io.py rename to strands-py/tests/strands/bidi/io/console/test_io.py index d950cd000..60a993d7f 100644 --- a/strands-py/tests/strands/experimental/bidi/io/console/test_io.py +++ b/strands-py/tests/strands/bidi/io/console/test_io.py @@ -5,8 +5,8 @@ from unittest.mock import Mock import pytest import pytest_asyncio -from strands.experimental.bidi.io import ConsoleIO, ConsoleIOConfig -from strands.experimental.bidi.types import ( +from strands.bidi.io import ConsoleIO, ConsoleIOConfig +from strands.bidi.types import ( BidiConnectionStopEvent, BidiReasoningDeltaEvent, BidiReasoningStartEvent, diff --git a/strands-py/tests/strands/experimental/bidi/io/console/test_keyboard.py b/strands-py/tests/strands/bidi/io/console/test_keyboard.py similarity index 94% rename from strands-py/tests/strands/experimental/bidi/io/console/test_keyboard.py rename to strands-py/tests/strands/bidi/io/console/test_keyboard.py index afe9d49e7..e8edaf769 100644 --- a/strands-py/tests/strands/experimental/bidi/io/console/test_keyboard.py +++ b/strands-py/tests/strands/bidi/io/console/test_keyboard.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, Mock import pytest import pytest_asyncio -from strands.experimental.bidi.io.console._keyboard import Keyboard, KeyboardInput +from strands.bidi.io.console._keyboard import Keyboard, KeyboardInput @pytest_asyncio.fixture diff --git a/strands-py/tests/strands/experimental/bidi/io/test_audio.py b/strands-py/tests/strands/bidi/io/test_audio.py similarity index 97% rename from strands-py/tests/strands/experimental/bidi/io/test_audio.py rename to strands-py/tests/strands/bidi/io/test_audio.py index 074dc9f43..e2ef8dd1c 100644 --- a/strands-py/tests/strands/experimental/bidi/io/test_audio.py +++ b/strands-py/tests/strands/bidi/io/test_audio.py @@ -11,10 +11,10 @@ import numpy as np import pytest import pytest_asyncio -import strands.experimental.bidi.io as bidi_io -from strands.experimental.bidi.io import AudioIO, AudioProcessorConfig, ConsoleIO -from strands.experimental.bidi.models import AudioCapable -from strands.experimental.bidi.types import ( +import strands.bidi.io as bidi_io +from strands.bidi.io import AudioIO, AudioProcessorConfig, ConsoleIO +from strands.bidi.models import AudioCapable +from strands.bidi.types import ( AudioDelta, BidiAudioDeltaEvent, BidiBargeInEvent, @@ -29,7 +29,7 @@ def test_io_rejects_unknown_export(): def test_bidi_root_does_not_import_optional_dependencies(): - project_root = Path(__file__).resolve().parents[5] + project_root = Path(__file__).resolve().parents[4] env = os.environ.copy() python_path = str(project_root / "src") if existing_python_path := env.get("PYTHONPATH"): @@ -39,7 +39,7 @@ def test_bidi_root_does_not_import_optional_dependencies(): code = """ import sys -import strands.experimental.bidi +import strands.bidi optional_modules = ( "aws_sdk_bedrock_runtime", @@ -95,7 +95,7 @@ def pyaudio_module(): module.paInt16 = 8 module.paContinue = 0 module.get_sample_size.return_value = 2 - with unittest.mock.patch("strands.experimental.bidi.io.audio.pyaudio", module): + with unittest.mock.patch("strands.bidi.io.audio.pyaudio", module): yield module @@ -362,7 +362,7 @@ def test_audio_processor_shared_between_input_and_output(): def test_audio_module_import_error_includes_install_instruction(): - module_name = "strands.experimental.bidi._audio.processor" + module_name = "strands.bidi._audio.processor" module = importlib.import_module(module_name) sys.modules.pop(module_name) original_import = builtins.__import__ @@ -550,7 +550,7 @@ async def test_mixed_rate_reference_matches_mic_frame_length(py_audio, agent_mix # End-to-end regression for the output-rate bug: with a correctly sized output buffer, a 10ms speaker # frame at 24k resamples to exactly a 10ms mic frame at 16k (320 bytes), so the reference is fully real # audio with no zero-padding. - from strands.experimental.bidi.types import BidiAudioDeltaEvent + from strands.bidi.types import BidiAudioDeltaEvent processor = mock_audio_processor.return_value processor.process.return_value = np.zeros(160, dtype=np.int16) @@ -589,7 +589,7 @@ async def test_mixed_rate_reference_matches_mic_frame_length(py_audio, agent_mix @pytest.mark.asyncio async def test_output_records_reference_at_playback(py_audio, aec_agent, mock_audio_processor): - from strands.experimental.bidi.types import BidiAudioDeltaEvent + from strands.bidi.types import BidiAudioDeltaEvent audio_io = AudioIO(audio_processor=AudioProcessorConfig()) input_ = audio_io.input() @@ -622,7 +622,7 @@ async def test_output_records_reference_at_playback(py_audio, aec_agent, mock_au @pytest.mark.asyncio async def test_output_clears_reference_on_barge_in(py_audio, aec_agent, mock_audio_processor): - from strands.experimental.bidi.types import BidiAudioDeltaEvent, BidiBargeInEvent + from strands.bidi.types import BidiAudioDeltaEvent, BidiBargeInEvent audio_io = AudioIO(audio_processor=AudioProcessorConfig()) input_ = audio_io.input() diff --git a/strands-py/tests/strands/experimental/bidi/models/__init__.py b/strands-py/tests/strands/bidi/models/__init__.py similarity index 100% rename from strands-py/tests/strands/experimental/bidi/models/__init__.py rename to strands-py/tests/strands/bidi/models/__init__.py diff --git a/strands-py/tests/strands/experimental/bidi/models/test_bedrock.py b/strands-py/tests/strands/bidi/models/test_bedrock.py similarity index 99% rename from strands-py/tests/strands/experimental/bidi/models/test_bedrock.py rename to strands-py/tests/strands/bidi/models/test_bedrock.py index 1039dbe8d..c1bba9585 100644 --- a/strands-py/tests/strands/experimental/bidi/models/test_bedrock.py +++ b/strands-py/tests/strands/bidi/models/test_bedrock.py @@ -24,19 +24,19 @@ from aws_sdk_bedrock_runtime.models import ModelTimeoutException, ValidationExce from awscrt.exceptions import from_code from smithy_http.aio.crt import AWSCRTHTTPClient -from strands.experimental.bidi.models import ( +from strands.bidi.models import ( BedrockNovaSonicAudioConfig, BedrockNovaSonicModel, ConnectionTimeoutError, ) -from strands.experimental.bidi.models.bedrock import ( +from strands.bidi.models.bedrock import ( _BedrockAWSCRTHTTPClient, _BedrockAWSCRTHTTPResponse, _format_tool_result_after_restart, _ResponseState, _Transcript, ) -from strands.experimental.bidi.types import ( +from strands.bidi.types import ( AudioDelta, BidiAudioDeltaEvent, BidiAudioStartEvent, @@ -50,7 +50,7 @@ from strands.experimental.bidi.types import ( BidiTranscriptStopEvent, BidiUsageEvent, ) -from strands.experimental.bidi.types.events import BidiToolUseBlocksEvent +from strands.bidi.types.events import BidiToolUseBlocksEvent from strands.types.content import TextBlock from strands.types.media import ImageBlock from strands.types.tools import ToolResultBlock @@ -82,7 +82,7 @@ def mock_stream(): @pytest.fixture def mock_client(mock_stream): """Mock Bedrock Runtime client.""" - with patch("strands.experimental.bidi.models.bedrock.AsyncBedrockRuntimeClient") as mock_cls: + with patch("strands.bidi.models.bedrock.AsyncBedrockRuntimeClient") as mock_cls: mock_instance = AsyncMock() mock_instance.invoke_model_with_bidirectional_stream = AsyncMock(return_value=mock_stream) mock_cls.return_value = mock_instance @@ -209,7 +209,7 @@ async def test_restart_uses_updated_config(nova_model, mock_client, mock_stream) @pytest.mark.asyncio async def test_start_sets_strands_user_agent_on_bedrock_runtime_client(model_id, boto_session, mock_stream): """Always set the Strands user agent marker on the generated Bedrock Runtime client.""" - with patch("strands.experimental.bidi.models.bedrock.AsyncBedrockRuntimeClient") as mock_cls: + with patch("strands.bidi.models.bedrock.AsyncBedrockRuntimeClient") as mock_cls: mock_instance = AsyncMock() mock_instance.invoke_model_with_bidirectional_stream = AsyncMock(return_value=mock_stream) mock_cls.return_value = mock_instance @@ -1023,8 +1023,8 @@ async def test_proactive_reconnect_end_to_end_through_agent(model_id, boto_sessi and restarts through Nova's own restart() before the session deadline, replaying history via Nova's initialization path. No live AWS calls are made. """ - from strands.experimental.bidi.agent import BidiAgent - from strands.experimental.bidi.types import BidiConnectionWarningEvent + from strands.bidi.agent import BidiAgent + from strands.bidi.types import BidiConnectionWarningEvent # Nova never emits events on its own here; await_output blocks so the model task idles # while the proactive timer drives the reconnect. @@ -1078,7 +1078,7 @@ async def test_proactive_reconnect_end_to_end_through_agent(model_id, boto_sessi @pytest.mark.asyncio async def test_model_stop_after_start_failure(model_id, boto_session): - with patch("strands.experimental.bidi.models.bedrock.AsyncBedrockRuntimeClient") as mock_cls: + with patch("strands.bidi.models.bedrock.AsyncBedrockRuntimeClient") as mock_cls: mock_instance = AsyncMock() mock_instance.invoke_model_with_bidirectional_stream.side_effect = RuntimeError("connection failed") mock_cls.return_value = mock_instance @@ -2159,7 +2159,7 @@ async def test_tool_result_after_restart_sent_after_idle_timeout(nova_model, moc state = _reader_state(nova_model) nova_model._convert_nova_event(_USER_TEXT_START, state) - with patch("strands.experimental.bidi.models.bedrock._TOOL_RESULT_AFTER_RESTART_WAIT_S", 0): + with patch("strands.bidi.models.bedrock._TOOL_RESULT_AFTER_RESTART_WAIT_S", 0): await nova_model.send(BidiMessage([ToolResultBlock("call-1", "success", [{"text": "12:00"}])])) assert len(_sent_texts(mock_stream)) == 1 diff --git a/strands-py/tests/strands/experimental/bidi/models/test_configs.py b/strands-py/tests/strands/bidi/models/test_configs.py similarity index 94% rename from strands-py/tests/strands/experimental/bidi/models/test_configs.py rename to strands-py/tests/strands/bidi/models/test_configs.py index 9e66fa5ea..e0cac1468 100644 --- a/strands-py/tests/strands/experimental/bidi/models/test_configs.py +++ b/strands-py/tests/strands/bidi/models/test_configs.py @@ -4,9 +4,9 @@ import copy import pytest -from strands.experimental.bidi.models import AudioConfig, AudioStreamConfig, ModelConfig -from strands.experimental.bidi.models import configs as configs_module -from strands.experimental.bidi.models.configs import ( +from strands.bidi.models import AudioConfig, AudioStreamConfig, ModelConfig +from strands.bidi.models import configs as configs_module +from strands.bidi.models.configs import ( _merge_config, _validate_audio_config, _validate_model_config, diff --git a/strands-py/tests/strands/experimental/bidi/models/test_google.py b/strands-py/tests/strands/bidi/models/test_google.py similarity index 99% rename from strands-py/tests/strands/experimental/bidi/models/test_google.py rename to strands-py/tests/strands/bidi/models/test_google.py index 03581f58b..d5dd84134 100644 --- a/strands-py/tests/strands/experimental/bidi/models/test_google.py +++ b/strands-py/tests/strands/bidi/models/test_google.py @@ -15,10 +15,10 @@ import unittest.mock import pytest from google.genai import types as genai_types -import strands.experimental.bidi.agent.loop as loop_module -from strands.experimental.bidi.models import ConnectionTimeoutError, GoogleGeminiLiveAudioConfig, GoogleGeminiLiveModel -from strands.experimental.bidi.models.google import _TurnState -from strands.experimental.bidi.types import ( +import strands.bidi.agent.loop as loop_module +from strands.bidi.models import ConnectionTimeoutError, GoogleGeminiLiveAudioConfig, GoogleGeminiLiveModel +from strands.bidi.models.google import _TurnState +from strands.bidi.types import ( AudioDelta, BidiAudioDeltaEvent, BidiAudioStartEvent, @@ -48,7 +48,7 @@ from strands.types.tools import ToolResultBlock @pytest.fixture def mock_genai_client(): """Mock the Google GenAI client.""" - with unittest.mock.patch("strands.experimental.bidi.models.google.genai.Client") as mock_client_cls: + with unittest.mock.patch("strands.bidi.models.google.genai.Client") as mock_client_cls: mock_client = mock_client_cls.return_value mock_client.aio = unittest.mock.MagicMock() @@ -569,8 +569,8 @@ async def test_proactive_reconnect_end_to_end_through_agent(mock_genai_client, m through Gemini's own restart() before the deadline, resuming the session via its handle. No live network calls are made. """ - from strands.experimental.bidi.agent import BidiAgent - from strands.experimental.bidi.types import BidiConnectionWarningEvent + from strands.bidi.agent import BidiAgent + from strands.bidi.types import BidiConnectionWarningEvent mock_client, mock_live_session, _ = mock_genai_client @@ -1204,7 +1204,7 @@ def test_convert_gemini_live_event_completes_user_transcript_at_turn_end(model, def test_convert_gemini_live_event_separates_text_reasoning_and_transcripts(model): state = _TurnState() with unittest.mock.patch( - "strands.experimental.bidi.models.google.uuid.uuid4", + "strands.bidi.models.google.uuid.uuid4", side_effect=["transcript", "reasoning", "text", "audio", "response"], ): tru_events = model._convert_gemini_live_event( diff --git a/strands-py/tests/strands/experimental/bidi/models/test_model.py b/strands-py/tests/strands/bidi/models/test_model.py similarity index 93% rename from strands-py/tests/strands/experimental/bidi/models/test_model.py rename to strands-py/tests/strands/bidi/models/test_model.py index 88b62a0d3..886763241 100644 --- a/strands-py/tests/strands/experimental/bidi/models/test_model.py +++ b/strands-py/tests/strands/bidi/models/test_model.py @@ -6,8 +6,8 @@ from typing import Any import pytest from pydantic import BaseModel -from strands.experimental.bidi.models import AudioCapable, AudioConfig, BidiModel, Restartable -from strands.experimental.bidi.types import BidiContentDelta, BidiMessage, BidiOutputEvent +from strands.bidi.models import AudioCapable, AudioConfig, BidiModel, Restartable +from strands.bidi.types import BidiContentDelta, BidiMessage, BidiOutputEvent from strands.models import Model from strands.types.content import Messages from strands.types.tools import ToolSpec diff --git a/strands-py/tests/strands/experimental/bidi/models/test_openai.py b/strands-py/tests/strands/bidi/models/test_openai.py similarity index 99% rename from strands-py/tests/strands/experimental/bidi/models/test_openai.py rename to strands-py/tests/strands/bidi/models/test_openai.py index 35f1e4bd5..15484b1f5 100644 --- a/strands-py/tests/strands/experimental/bidi/models/test_openai.py +++ b/strands-py/tests/strands/bidi/models/test_openai.py @@ -16,15 +16,15 @@ import unittest.mock import pytest -from strands.experimental.bidi import BidiAgent -from strands.experimental.bidi.models import ConnectionTimeoutError, OpenAIRealtimeModel -from strands.experimental.bidi.models.openai import ( +from strands.bidi import BidiAgent +from strands.bidi.models import ConnectionTimeoutError, OpenAIRealtimeModel +from strands.bidi.models.openai import ( _RESTART_INSTRUCTION, OPENAI_MAX_TIMEOUT_S, OPENAI_PROACTIVE_RECONNECT_MARGIN_S, _SessionState, ) -from strands.experimental.bidi.types import ( +from strands.bidi.types import ( AudioDelta, BidiAudioDeltaEvent, BidiAudioStartEvent, @@ -65,7 +65,7 @@ def mock_websockets_connect(mock_websocket): async def async_connect(*args, **kwargs): return mock_websocket - with unittest.mock.patch("strands.experimental.bidi.models.openai.websockets.connect") as mock_connect: + with unittest.mock.patch("strands.bidi.models.openai.websockets.connect") as mock_connect: mock_connect.side_effect = async_connect yield mock_connect, mock_websocket @@ -806,7 +806,7 @@ async def test_receive_combines_assistant_content( await agent.stop() -@unittest.mock.patch("strands.experimental.bidi.models.openai.time.time") +@unittest.mock.patch("strands.bidi.models.openai.time.time") @pytest.mark.asyncio async def test_receive_timeout(mock_time, model): mock_time.side_effect = itertools.count() diff --git a/strands-py/tests/strands/experimental/bidi/types/__init__.py b/strands-py/tests/strands/bidi/types/__init__.py similarity index 100% rename from strands-py/tests/strands/experimental/bidi/types/__init__.py rename to strands-py/tests/strands/bidi/types/__init__.py diff --git a/strands-py/tests/strands/experimental/bidi/types/test_events.py b/strands-py/tests/strands/bidi/types/test_events.py similarity index 98% rename from strands-py/tests/strands/experimental/bidi/types/test_events.py rename to strands-py/tests/strands/bidi/types/test_events.py index 120ba3241..f94ce682c 100644 --- a/strands-py/tests/strands/experimental/bidi/types/test_events.py +++ b/strands-py/tests/strands/bidi/types/test_events.py @@ -8,7 +8,7 @@ import json import pytest -from strands.experimental.bidi.types import ( +from strands.bidi.types import ( BidiAudioDeltaEvent, BidiAudioStartEvent, BidiAudioStopEvent, @@ -32,7 +32,7 @@ from strands.experimental.bidi.types import ( BidiTranscriptStopEvent, BidiUsageEvent, ) -from strands.experimental.bidi.types.events import _normalize_role +from strands.bidi.types.events import _normalize_role @pytest.mark.parametrize( diff --git a/strands-py/tests/strands/experimental/bidi/test_deprecated_alias.py b/strands-py/tests/strands/experimental/bidi/test_deprecated_alias.py new file mode 100644 index 000000000..c49ef5b71 --- /dev/null +++ b/strands-py/tests/strands/experimental/bidi/test_deprecated_alias.py @@ -0,0 +1,297 @@ +"""Compatibility tests for the deprecated bidirectional streaming namespace.""" + +import importlib +import os +import pickle +import subprocess +import sys +import textwrap +import types +import unittest.mock +from pathlib import Path + +import pytest + +_MODULE_PATHS = ( + "_async", + "_async._task_group", + "_async._task_pool", + "_audio", + "_audio.buffer", + "_audio.processor", + "_telemetry", + "agent._blocks", + "agent._reconnect_timer", + "agent.agent", + "agent.loop", + "hooks.events", + "io.audio", + "io.configs", + "io.console", + "io.console._display", + "io.console._io", + "io.console._keyboard", + "models.configs", + "models.google", + "models.model", + "models.openai", + "types.agent", + "types.content", + "types.events", + "types.io", + "types.media", +) + +_OWNER_MODULES = ("agent", "hooks", "io", "models", "types") + + +def _subprocess_env() -> dict[str, str]: + project_root = Path(__file__).resolve().parents[4] + python_path = str(project_root / "src") + env = os.environ.copy() + if existing_python_path := env.get("PYTHONPATH"): + python_path = os.pathsep.join((python_path, existing_python_path)) + env["PYTHONPATH"] = python_path + return env + + +@pytest.fixture +def pyaudio_stub(monkeypatch): + """Stand in for the native PyAudio dependency, which the test environment does not install.""" + pyaudio = types.ModuleType("pyaudio") + pyaudio.PyAudio = object + pyaudio.Stream = object + monkeypatch.setitem(sys.modules, "pyaudio", pyaudio) + + +@pytest.mark.parametrize("module_path", _MODULE_PATHS) +def test_leaf_module_is_stable_module(module_path, pyaudio_stub): + old_module = importlib.import_module(f"strands.experimental.bidi.{module_path}") + new_module = importlib.import_module(f"strands.bidi.{module_path}") + + assert old_module is new_module + + +def test_missing_submodule_raises_for_deprecated_name(): + with pytest.raises(ModuleNotFoundError) as error: + importlib.import_module("strands.experimental.bidi.models.missing") + + assert error.value.name == "strands.experimental.bidi.models.missing" + + +@pytest.mark.parametrize("owner_name", _OWNER_MODULES) +def test_owner_package_is_shim(owner_name): + old_owner = importlib.import_module(f"strands.experimental.bidi.{owner_name}") + new_owner = importlib.import_module(f"strands.bidi.{owner_name}") + + assert old_owner is not new_owner + assert Path(old_owner.__file__).parent.name == owner_name + assert "experimental" in Path(old_owner.__file__).parts + + +def test_root_and_owner_exports_are_same_objects(pyaudio_stub): + old_root = importlib.import_module("strands.experimental.bidi") + new_root = importlib.import_module("strands.bidi") + + assert old_root.BidiAgent is new_root.BidiAgent + assert old_root.__all__ == ["agent", "hooks", "io", "models", "types"] + + for owner_name in _OWNER_MODULES: + old_owner = importlib.import_module(f"strands.experimental.bidi.{owner_name}") + new_owner = importlib.import_module(f"strands.bidi.{owner_name}") + assert old_owner.__all__ == new_owner.__all__ + for public_name in new_owner.__all__: + assert getattr(old_owner, public_name) is getattr(new_owner, public_name) + + +def test_lazy_model_exports_are_same_objects(): + old_models = importlib.import_module("strands.experimental.bidi.models") + new_models = importlib.import_module("strands.bidi.models") + + for model_name in ("GoogleGeminiLiveModel", "OpenAIRealtimeModel"): + assert getattr(old_models, model_name) is getattr(new_models, model_name) + + +def test_bedrock_alias_matches_supported_version_behavior(): + new_module_name = "strands.bidi.models.bedrock" + old_module_name = "strands.experimental.bidi.models.bedrock" + + if sys.version_info >= (3, 12): + assert importlib.import_module(old_module_name) is importlib.import_module(new_module_name) + return + + with pytest.raises(ImportError) as new_error: + importlib.import_module(new_module_name) + with pytest.raises(ImportError) as old_error: + importlib.import_module(old_module_name) + + assert str(old_error.value) == str(new_error.value) + + +def test_old_leaf_patch_target_updates_stable_module(): + new_module = importlib.import_module("strands.bidi.models.openai") + + with unittest.mock.patch( + "strands.experimental.bidi.models.openai.time.time", + return_value=123.0, + ): + assert new_module.time.time() == 123.0 + + +def test_old_pickle_module_path_resolves_stable_class(): + from strands.bidi.types import AudioDelta + + serialized_class = b"cstrands.experimental.bidi.types.media\nAudioDelta\n." + + assert pickle.loads(serialized_class) is AudioDelta + + +def test_old_root_star_import_keeps_historical_exports(): + namespace: dict[str, object] = {} + + exec("from strands.experimental.bidi import *", namespace) + + assert "BidiAgent" not in namespace + assert set(_OWNER_MODULES).issubset(namespace) + + +def test_import_order_preserves_leaf_identity(tmp_path): + script = tmp_path / "check_import_order.py" + script.write_text( + textwrap.dedent( + """ + import importlib + import sys + + old_first = sys.argv[1] == "old" + first = "strands.experimental.bidi" if old_first else "strands.bidi" + second = "strands.bidi" if old_first else "strands.experimental.bidi" + importlib.import_module(first) + importlib.import_module(second) + + old = importlib.import_module("strands.experimental.bidi.models.openai") + new = importlib.import_module("strands.bidi.models.openai") + assert old is new + """ + ), + encoding="utf-8", + ) + + for first_namespace in ("old", "new"): + subprocess.run( + [sys.executable, str(script), first_namespace], + check=True, + env=_subprocess_env(), + ) + + +def test_deprecation_warning_fires_once_at_importer(tmp_path): + script = tmp_path / "check_warning.py" + script.write_text( + textwrap.dedent( + """ + import warnings + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + import strands.experimental.bidi + import strands.experimental.bidi.io + import strands.experimental.bidi.models.openai + + bidi_warnings = [w for w in caught if "strands.experimental.bidi" in str(w.message)] + assert len(bidi_warnings) == 1, bidi_warnings + warning = bidi_warnings[0] + assert warning.category is DeprecationWarning + assert "strands.experimental.bidi is deprecated" in str(warning.message) + assert "strands.bidi" in str(warning.message) + assert "v1.60.0" in str(warning.message) + assert warning.filename == __file__, warning.filename + """ + ), + encoding="utf-8", + ) + + subprocess.run([sys.executable, str(script)], check=True, env=_subprocess_env()) + + +def test_old_package_imports_do_not_load_optional_dependencies(tmp_path): + script = tmp_path / "check_optional_dependencies.py" + script.write_text( + textwrap.dedent( + """ + import sys + + import strands.experimental.bidi + import strands.experimental.bidi.io + + optional_modules = ( + "aws_sdk_bedrock_runtime", + "google.genai", + "prompt_toolkit", + "pyaudio", + "websockets", + ) + loaded = [name for name in optional_modules if name in sys.modules] + assert not loaded, loaded + """ + ), + encoding="utf-8", + ) + + subprocess.run([sys.executable, str(script)], check=True, env=_subprocess_env()) + + +def test_stable_import_does_not_load_deprecated_namespace(tmp_path): + script = tmp_path / "check_stable_import.py" + script.write_text( + textwrap.dedent( + """ + import sys + import warnings + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + import strands + import strands.bidi + + assert "strands.experimental.bidi" not in sys.modules + bidi_warnings = [w for w in caught if "strands.experimental.bidi" in str(w.message)] + assert not bidi_warnings, bidi_warnings + """ + ), + encoding="utf-8", + ) + + subprocess.run([sys.executable, str(script)], check=True, env=_subprocess_env()) + + +def test_repository_does_not_import_deprecated_namespace(): + project_root = Path(__file__).resolve().parents[4] + source_root = project_root / "src" + shim_root = source_root / "strands" / "experimental" / "bidi" + excluded_files = { + Path(__file__).resolve(), + project_root / "tests_typing" / "bidi" / "test_deprecated_alias.py", + } + forbidden_patterns = ( + "strands.experimental.bidi", + ".experimental.bidi", + ".experimental import bidi", + ) + violations = [] + + for scan_root in ( + source_root, + project_root / "tests", + project_root / "tests_integ", + project_root / "tests_typing", + ): + for source_file in scan_root.rglob("*.py"): + resolved_file = source_file.resolve() + if resolved_file in excluded_files or resolved_file.is_relative_to(shim_root): + continue + source = source_file.read_text(encoding="utf-8") + if any(pattern in source for pattern in forbidden_patterns): + violations.append(str(source_file.relative_to(project_root))) + + assert violations == [] diff --git a/strands-py/tests/strands/session/test_repository_session_manager.py b/strands-py/tests/strands/session/test_repository_session_manager.py index ef89e868e..98bfe085d 100644 --- a/strands-py/tests/strands/session/test_repository_session_manager.py +++ b/strands-py/tests/strands/session/test_repository_session_manager.py @@ -10,7 +10,7 @@ from strands.agent.conversation_manager.null_conversation_manager import NullCon from strands.agent.conversation_manager.sliding_window_conversation_manager import SlidingWindowConversationManager from strands.agent.conversation_manager.summarizing_conversation_manager import SummarizingConversationManager from strands.agent.state import AgentState -from strands.experimental.bidi.agent import BidiAgent +from strands.bidi.agent import BidiAgent from strands.hooks import AfterInvocationEvent from strands.interrupt import _InterruptState from strands.session.file_session_manager import FileSessionManager diff --git a/strands-py/tests/strands/session/test_snapshot_session_manager.py b/strands-py/tests/strands/session/test_snapshot_session_manager.py index 00a835baa..4118e6633 100644 --- a/strands-py/tests/strands/session/test_snapshot_session_manager.py +++ b/strands-py/tests/strands/session/test_snapshot_session_manager.py @@ -13,8 +13,8 @@ from strands._context_manager.context_manager import ContextManager from strands.agent import AgentResult from strands.agent.agent import Agent from strands.agent.conversation_manager.sliding_window_conversation_manager import SlidingWindowConversationManager -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.models import BidiModel +from strands.bidi.agent import BidiAgent +from strands.bidi.models import BidiModel from strands.hooks.events import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, diff --git a/strands-py/tests/strands/tools/test_caller.py b/strands-py/tests/strands/tools/test_caller.py index c1fd1581d..d510b1e30 100644 --- a/strands-py/tests/strands/tools/test_caller.py +++ b/strands-py/tests/strands/tools/test_caller.py @@ -5,8 +5,8 @@ import weakref import pytest from strands import Agent, tool -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.models import BidiModel +from strands.bidi.agent import BidiAgent +from strands.bidi.models import BidiModel from strands.tools.tool_provider import ToolProvider from strands.types.exceptions import ConcurrencyException diff --git a/strands-py/tests/strands/types/test_session.py b/strands-py/tests/strands/types/test_session.py index 481f32bcc..d25ec6d53 100644 --- a/strands-py/tests/strands/types/test_session.py +++ b/strands-py/tests/strands/types/test_session.py @@ -7,7 +7,7 @@ import pytest from strands import Agent from strands.agent.conversation_manager.null_conversation_manager import NullConversationManager from strands.agent.state import AgentState -from strands.experimental.bidi.agent import BidiAgent +from strands.bidi.agent import BidiAgent from strands.interrupt import _InterruptState from strands.types.session import ( Session, diff --git a/strands-py/tests_integ/bidi/context.py b/strands-py/tests_integ/bidi/context.py index e7edd2fae..1ddcc70bf 100644 --- a/strands-py/tests_integ/bidi/context.py +++ b/strands-py/tests_integ/bidi/context.py @@ -10,7 +10,7 @@ import logging from typing import TYPE_CHECKING if TYPE_CHECKING: - from strands.experimental.bidi.agent import BidiAgent + from strands.bidi.agent import BidiAgent from .generators.audio import AudioGenerator diff --git a/strands-py/tests_integ/bidi/hook_utils.py b/strands-py/tests_integ/bidi/hook_utils.py index a060290bf..985c25a87 100644 --- a/strands-py/tests_integ/bidi/hook_utils.py +++ b/strands-py/tests_integ/bidi/hook_utils.py @@ -1,7 +1,7 @@ """Shared utilities for testing BidiAgent hooks.""" from strands import LocalAgent -from strands.experimental.bidi.hooks import ( +from strands.bidi.hooks import ( BidiAgentStopEvent, BidiBargeInEvent, BidiResponseStopEvent, diff --git a/strands-py/tests_integ/bidi/test_bidi_hooks.py b/strands-py/tests_integ/bidi/test_bidi_hooks.py index 3d3042db7..7bc12244e 100644 --- a/strands-py/tests_integ/bidi/test_bidi_hooks.py +++ b/strands-py/tests_integ/bidi/test_bidi_hooks.py @@ -3,8 +3,8 @@ import pytest from strands import tool -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.hooks import BidiAgentStopEvent +from strands.bidi.agent import BidiAgent +from strands.bidi.hooks import BidiAgentStopEvent from strands.hooks import AgentInitializedEvent, HookProvider from .context import BidirectionalTestContext diff --git a/strands-py/tests_integ/bidi/test_bidirectional_agent.py b/strands-py/tests_integ/bidi/test_bidirectional_agent.py index 1059591b7..7fb2f3cb2 100644 --- a/strands-py/tests_integ/bidi/test_bidirectional_agent.py +++ b/strands-py/tests_integ/bidi/test_bidirectional_agent.py @@ -13,11 +13,11 @@ import os import pytest from strands import tool -from strands.experimental.bidi.agent import BidiAgent -from strands.experimental.bidi.hooks import BidiResponseStopEvent -from strands.experimental.bidi.models import GoogleGeminiLiveModel, OpenAIRealtimeModel -from strands.experimental.bidi.types import BidiResponseStartEvent, BidiTranscriptBlockEvent -from strands.experimental.bidi.types import BidiResponseStopEvent as BidiResponseStopStreamEvent +from strands.bidi.agent import BidiAgent +from strands.bidi.hooks import BidiResponseStopEvent +from strands.bidi.models import GoogleGeminiLiveModel, OpenAIRealtimeModel +from strands.bidi.types import BidiResponseStartEvent, BidiTranscriptBlockEvent +from strands.bidi.types import BidiResponseStopEvent as BidiResponseStopStreamEvent from strands.types._events import ToolResultEvent from strands.types.media import ImageBlock @@ -29,7 +29,7 @@ logger = logging.getLogger(__name__) def create_bedrock_nova_sonic_model(**kwargs): """Create a Nova Sonic model without importing its Python 3.12-only SDK during collection.""" - from strands.experimental.bidi.models import BedrockNovaSonicModel + from strands.bidi.models import BedrockNovaSonicModel return BedrockNovaSonicModel(**kwargs) diff --git a/strands-py/tests_integ/bidi/tools/test_direct.py b/strands-py/tests_integ/bidi/tools/test_direct.py index 7d5c70977..0e375f3c3 100644 --- a/strands-py/tests_integ/bidi/tools/test_direct.py +++ b/strands-py/tests_integ/bidi/tools/test_direct.py @@ -3,7 +3,7 @@ import unittest.mock import pytest from strands import tool -from strands.experimental.bidi.agent import BidiAgent +from strands.bidi.agent import BidiAgent @pytest.fixture diff --git a/strands-py/tests_typing/bidi/test_deprecated_alias.py b/strands-py/tests_typing/bidi/test_deprecated_alias.py new file mode 100644 index 000000000..ef2f8b467 --- /dev/null +++ b/strands-py/tests_typing/bidi/test_deprecated_alias.py @@ -0,0 +1,24 @@ +"""Typing compatibility for the deprecated bidirectional streaming namespace.""" + +from typing_extensions import assert_type + +from strands.bidi import BidiAgent as StableBidiAgent +from strands.bidi.io import AudioIO as StableAudioIO +from strands.bidi.models import OpenAIRealtimeModel as StableOpenAIRealtimeModel +from strands.bidi.types import BidiMessage as StableBidiMessage +from strands.experimental.bidi import BidiAgent as DeprecatedBidiAgent +from strands.experimental.bidi.io import AudioIO as DeprecatedAudioIO +from strands.experimental.bidi.models import OpenAIRealtimeModel as DeprecatedOpenAIRealtimeModel +from strands.experimental.bidi.types import BidiMessage as DeprecatedBidiMessage + + +def deprecated_imports_keep_stable_types( + agent: DeprecatedBidiAgent, + audio_io: DeprecatedAudioIO, + model: DeprecatedOpenAIRealtimeModel, + message: DeprecatedBidiMessage, +) -> None: + assert_type(agent, StableBidiAgent) + assert_type(audio_io, StableAudioIO) + assert_type(model, StableOpenAIRealtimeModel) + assert_type(message, StableBidiMessage) diff --git a/strands-py/tests_typing/bidi/test_model.py b/strands-py/tests_typing/bidi/test_model.py index 422b64c60..91b62820b 100644 --- a/strands-py/tests_typing/bidi/test_model.py +++ b/strands-py/tests_typing/bidi/test_model.py @@ -1,6 +1,6 @@ from typing_extensions import assert_type -from strands.experimental.bidi.models import ( +from strands.bidi.models import ( BedrockNovaSonicModel, GoogleGeminiLiveModel, ModelConfig, diff --git a/strands-py/tests_typing/test_local_agent.py b/strands-py/tests_typing/test_local_agent.py index 5651eea32..5fd71af92 100644 --- a/strands-py/tests_typing/test_local_agent.py +++ b/strands-py/tests_typing/test_local_agent.py @@ -5,7 +5,7 @@ from typing_extensions import assert_type from strands import Agent, LocalAgent, Snapshot, ToolContext, tool from strands._context_manager.context_manager import ContextManager -from strands.experimental.bidi.agent import BidiAgent +from strands.bidi.agent import BidiAgent from strands.hooks import AfterToolCallEvent, AgentInitializedEvent, BeforeToolCallEvent, MessageAddedEvent from strands.sandbox import Sandbox from strands.session.repository_session_manager import RepositorySessionManager