Skip to content

Commit a70a624

Browse files
committed
Merge branch 'main' of https://github.com/strands-agents/sdk-python into swarm-interrupt
2 parents 2b5a99c + 62534de commit a70a624

File tree

98 files changed

+11424
-195
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

98 files changed

+11424
-195
lines changed

.github/workflows/test-lint.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,20 @@ jobs:
5959
uses: actions/setup-python@v6
6060
with:
6161
python-version: ${{ matrix.python-version }}
62+
- name: Install system audio dependencies (Linux)
63+
if: matrix.os-name == 'linux'
64+
run: |
65+
sudo apt-get update
66+
sudo apt-get install -y portaudio19-dev libasound2-dev
67+
- name: Install system audio dependencies (macOS)
68+
if: matrix.os-name == 'macOS'
69+
run: |
70+
brew install portaudio
71+
- name: Install system audio dependencies (Windows)
72+
if: matrix.os-name == 'windows'
73+
run: |
74+
# Windows typically has audio libraries available by default
75+
echo "Windows audio dependencies handled by PyAudio wheels"
6276
- name: Install dependencies
6377
run: |
6478
pip install --no-cache-dir hatch
@@ -89,6 +103,11 @@ jobs:
89103
python-version: '3.10'
90104
cache: 'pip'
91105

106+
- name: Install system audio dependencies (Linux)
107+
run: |
108+
sudo apt-get update
109+
sudo apt-get install -y portaudio19-dev libasound2-dev
110+
92111
- name: Install dependencies
93112
run: |
94113
pip install --no-cache-dir hatch
@@ -97,3 +116,4 @@ jobs:
97116
id: lint
98117
run: hatch fmt --linter --check
99118
continue-on-error: false
119+

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ dist
1313
repl_state
1414
.kiro
1515
uv.lock
16+
.audio_cache

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,74 @@ agent("What is the square root of 1764")
197197

198198
It's also available on GitHub via [strands-agents/tools](https://github.com/strands-agents/tools).
199199

200+
### Bidirectional Streaming
201+
202+
> **⚠️ Experimental Feature**: Bidirectional streaming is currently in experimental status. APIs may change in future releases as we refine the feature based on user feedback and evolving model capabilities.
203+
204+
Build real-time voice and audio conversations with persistent streaming connections. Unlike traditional request-response patterns, bidirectional streaming maintains long-running conversations where users can interrupt, provide continuous input, and receive real-time audio responses. Get started with your first BidiAgent by following the [Quickstart](https://strandsagents.com/latest/documentation/docs/user-guide/concepts/experimental/bidirectional-streaming/quickstart) guide.
205+
206+
**Supported Model Providers:**
207+
- Amazon Nova Sonic (`amazon.nova-sonic-v1:0`)
208+
- Google Gemini Live (`gemini-2.5-flash-native-audio-preview-09-2025`)
209+
- OpenAI Realtime API (`gpt-realtime`)
210+
211+
**Quick Example:**
212+
213+
```python
214+
import asyncio
215+
from strands.experimental.bidi import BidiAgent
216+
from strands.experimental.bidi.models import BidiNovaSonicModel
217+
from strands.experimental.bidi.io import BidiAudioIO, BidiTextIO
218+
from strands.experimental.bidi.tools import stop_conversation
219+
from strands_tools import calculator
220+
221+
async def main():
222+
# Create bidirectional agent with audio model
223+
model = BidiNovaSonicModel()
224+
agent = BidiAgent(model=model, tools=[calculator, stop_conversation])
225+
226+
# Setup audio and text I/O
227+
audio_io = BidiAudioIO()
228+
text_io = BidiTextIO()
229+
230+
# Run with real-time audio streaming
231+
# Say "stop conversation" to gracefully end the conversation
232+
await agent.run(
233+
inputs=[audio_io.input()],
234+
outputs=[audio_io.output(), text_io.output()]
235+
)
236+
237+
if __name__ == "__main__":
238+
asyncio.run(main())
239+
```
240+
241+
**Configuration Options:**
242+
243+
```python
244+
# Configure audio settings
245+
model = BidiNovaSonicModel(
246+
provider_config={
247+
"audio": {
248+
"input_rate": 16000,
249+
"output_rate": 16000,
250+
"voice": "matthew"
251+
},
252+
"inference": {
253+
"max_tokens": 2048,
254+
"temperature": 0.7
255+
}
256+
}
257+
)
258+
259+
# Configure I/O devices
260+
audio_io = BidiAudioIO(
261+
input_device_index=0, # Specific microphone
262+
output_device_index=1, # Specific speaker
263+
input_buffer_size=10,
264+
output_buffer_size=10
265+
)
266+
```
267+
200268
## Documentation
201269

202270
For detailed guidance & examples, explore our documentation:

pyproject.toml

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,18 @@ a2a = [
6969
"fastapi>=0.115.12,<1.0.0",
7070
"starlette>=0.46.2,<1.0.0",
7171
]
72+
73+
bidi = [
74+
"aws_sdk_bedrock_runtime; python_version>='3.12'",
75+
"prompt_toolkit>=3.0.0,<4.0.0",
76+
"pyaudio>=0.2.13,<1.0.0",
77+
"smithy-aws-core>=0.0.1; python_version>='3.12'",
78+
]
79+
bidi-gemini = ["google-genai>=1.32.0,<2.0.0"]
80+
bidi-openai = ["websockets>=15.0.0,<16.0.0"]
81+
7282
all = ["strands-agents[a2a,anthropic,docs,gemini,litellm,llamaapi,mistral,ollama,openai,writer,sagemaker,otel]"]
83+
bidi-all = ["strands-agents[a2a,bidi,bidi-gemini,bidi-openai,docs,otel]"]
7384

7485
dev = [
7586
"commitizen>=4.4.0,<5.0.0",
@@ -104,9 +115,10 @@ features = ["all"]
104115
dependencies = [
105116
"mypy>=1.15.0,<2.0.0",
106117
"ruff>=0.13.0,<0.14.0",
107-
# Include required pacakge dependencies for mypy
118+
# Include required package dependencies for mypy
108119
"strands-agents @ {root:uri}",
109120
]
121+
python = "3.10"
110122

111123
# Define static-analysis scripts so we can include mypy as part of the linting check
112124
[tool.hatch.envs.hatch-static-analysis.scripts]
@@ -118,7 +130,7 @@ format-fix = [
118130
]
119131
lint-check = [
120132
"ruff check",
121-
"mypy -p src"
133+
"mypy ./src"
122134
]
123135
lint-fix = [
124136
"ruff check --fix"
@@ -192,11 +204,16 @@ warn_no_return = true
192204
warn_unreachable = true
193205
follow_untyped_imports = true
194206
ignore_missing_imports = false
207+
exclude = ["src/strands/experimental/bidi"]
195208

209+
[[tool.mypy.overrides]]
210+
module = ["strands.experimental.bidi.*"]
211+
follow_imports = "skip"
196212

197213
[tool.ruff]
198214
line-length = 120
199215
include = ["examples/**/*.py", "src/**/*.py", "tests/**/*.py", "tests_integ/**/*.py"]
216+
exclude = ["src/strands/experimental/bidi/**/*.py", "tests/strands/experimental/bidi/**/*.py", "tests_integ/bidi/**/*.py"]
200217

201218
[tool.ruff.lint]
202219
select = [
@@ -219,6 +236,7 @@ convention = "google"
219236
[tool.pytest.ini_options]
220237
testpaths = ["tests"]
221238
asyncio_default_fixture_loop_scope = "function"
239+
addopts = "--ignore=tests/strands/experimental/bidi --ignore=tests_integ/bidi"
222240

223241

224242
[tool.coverage.run]
@@ -227,6 +245,7 @@ source = ["src"]
227245
context = "thread"
228246
parallel = true
229247
concurrency = ["thread", "multiprocessing"]
248+
omit = ["src/strands/experimental/bidi/*"]
230249

231250
[tool.coverage.report]
232251
show_missing = true
@@ -256,3 +275,48 @@ style = [
256275
["text", ""],
257276
["disabled", "fg:#858585 italic"]
258277
]
278+
279+
# =========================
280+
# Bidi development configs
281+
# =========================
282+
283+
[tool.hatch.envs.bidi]
284+
dev-mode = true
285+
features = ["dev", "bidi-all"]
286+
installer = "uv"
287+
288+
[tool.hatch.envs.bidi.scripts]
289+
prepare = [
290+
"hatch run bidi-lint:format-fix",
291+
"hatch run bidi-lint:quality-fix",
292+
"hatch run bidi-lint:type-check",
293+
"hatch run bidi-test:test-cov",
294+
]
295+
296+
[tools.hatch.envs.bidi-lint]
297+
template = "bidi"
298+
299+
[tool.hatch.envs.bidi-lint.scripts]
300+
format-check = "format-fix --check"
301+
format-fix = "ruff format {args} --target-version py312 ./src/strands/experimental/bidi/**/*.py"
302+
quality-check = "ruff check {args} --target-version py312 ./src/strands/experimental/bidi/**/*.py"
303+
quality-fix = "quality-check --fix"
304+
type-check = "mypy {args} --python-version 3.12 ./src/strands/experimental/bidi/**/*.py"
305+
306+
[tool.hatch.envs.bidi-test]
307+
template = "bidi"
308+
309+
[tool.hatch.envs.bidi-test.scripts]
310+
test = "pytest {args} tests/strands/experimental/bidi"
311+
test-cov = """
312+
test \
313+
--cov=strands.experimental.bidi \
314+
--cov-config= \
315+
--cov-branch \
316+
--cov-report=term-missing \
317+
--cov-report=xml:build/coverage/bidi-coverage.xml \
318+
--cov-report=html:build/coverage/bidi-html
319+
"""
320+
321+
[[tool.hatch.envs.bidi-test.matrix]]
322+
python = ["3.13", "3.12"]

src/strands/agent/state.py

Lines changed: 3 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,6 @@
11
"""Agent state management."""
22

3-
import copy
4-
import json
5-
from typing import Any, Dict, Optional
3+
from ..types.json_dict import JSONSerializableDict
64

7-
8-
class AgentState:
9-
"""Represents an Agent's stateful information outside of context provided to a model.
10-
11-
Provides a key-value store for agent state with JSON serialization validation and persistence support.
12-
Key features:
13-
- JSON serialization validation on assignment
14-
- Get/set/delete operations
15-
"""
16-
17-
def __init__(self, initial_state: Optional[Dict[str, Any]] = None):
18-
"""Initialize AgentState."""
19-
self._state: Dict[str, Dict[str, Any]]
20-
if initial_state:
21-
self._validate_json_serializable(initial_state)
22-
self._state = copy.deepcopy(initial_state)
23-
else:
24-
self._state = {}
25-
26-
def set(self, key: str, value: Any) -> None:
27-
"""Set a value in the state.
28-
29-
Args:
30-
key: The key to store the value under
31-
value: The value to store (must be JSON serializable)
32-
33-
Raises:
34-
ValueError: If key is invalid, or if value is not JSON serializable
35-
"""
36-
self._validate_key(key)
37-
self._validate_json_serializable(value)
38-
39-
self._state[key] = copy.deepcopy(value)
40-
41-
def get(self, key: Optional[str] = None) -> Any:
42-
"""Get a value or entire state.
43-
44-
Args:
45-
key: The key to retrieve (if None, returns entire state object)
46-
47-
Returns:
48-
The stored value, entire state dict, or None if not found
49-
"""
50-
if key is None:
51-
return copy.deepcopy(self._state)
52-
else:
53-
# Return specific key
54-
return copy.deepcopy(self._state.get(key))
55-
56-
def delete(self, key: str) -> None:
57-
"""Delete a specific key from the state.
58-
59-
Args:
60-
key: The key to delete
61-
"""
62-
self._validate_key(key)
63-
64-
self._state.pop(key, None)
65-
66-
def _validate_key(self, key: str) -> None:
67-
"""Validate that a key is valid.
68-
69-
Args:
70-
key: The key to validate
71-
72-
Raises:
73-
ValueError: If key is invalid
74-
"""
75-
if key is None:
76-
raise ValueError("Key cannot be None")
77-
if not isinstance(key, str):
78-
raise ValueError("Key must be a string")
79-
if not key.strip():
80-
raise ValueError("Key cannot be empty")
81-
82-
def _validate_json_serializable(self, value: Any) -> None:
83-
"""Validate that a value is JSON serializable.
84-
85-
Args:
86-
value: The value to validate
87-
88-
Raises:
89-
ValueError: If value is not JSON serializable
90-
"""
91-
try:
92-
json.dumps(value)
93-
except (TypeError, ValueError) as e:
94-
raise ValueError(
95-
f"Value is not JSON serializable: {type(value).__name__}. "
96-
f"Only JSON-compatible types (str, int, float, bool, list, dict, None) are allowed."
97-
) from e
5+
# Type alias for agent state
6+
AgentState = JSONSerializableDict

src/strands/experimental/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This module implements experimental features that are subject to change in future revisions without notice.
44
"""
55

6-
from . import tools
6+
from . import steering, tools
77
from .agent_config import config_to_agent
88

9-
__all__ = ["config_to_agent", "tools"]
9+
__all__ = ["config_to_agent", "tools", "steering"]

0 commit comments

Comments
 (0)