diff --git a/agent/bedrock_adapter.py b/agent/bedrock_adapter.py index a09e1bc5d82..d5dab7bafff 100644 --- a/agent/bedrock_adapter.py +++ b/agent/bedrock_adapter.py @@ -58,17 +58,34 @@ _bedrock_runtime_client_cache: Dict[str, Any] = {} _bedrock_control_client_cache: Dict[str, Any] = {} +_MIN_BOTO3_VERSION = (1, 34, 59) + + def _require_boto3(): - """Import boto3, raising a clear error if not installed.""" + """Import boto3, raising a clear error if not installed or too old.""" try: import boto3 - return boto3 except ImportError: raise ImportError( "The 'boto3' package is required for the AWS Bedrock provider. " "Install it with: pip install boto3\n" "Or install Hermes with Bedrock support: pip install -e '.[bedrock]'" ) + # converse() / converse_stream() were added in boto3 1.34.59. + # When Hermes is installed editable into system Python, the system boto3 + # (e.g. Ubuntu 24.04 ships 1.34.46) may take precedence over the venv + # version pinned in pyproject.toml. + try: + version = tuple(int(x) for x in boto3.__version__.split(".")[:3]) + except (AttributeError, ValueError): + return boto3 # can't parse — don't block on version check + if version < _MIN_BOTO3_VERSION: + raise RuntimeError( + f"boto3 {boto3.__version__} does not support converse_stream " + f"(minimum 1.34.59 required). Upgrade with: " + f"pip install --upgrade boto3" + ) + return boto3 def _get_bedrock_runtime_client(region: str): diff --git a/tests/agent/test_bedrock_adapter.py b/tests/agent/test_bedrock_adapter.py index f8190bf0d71..ac2b557aeac 100644 --- a/tests/agent/test_bedrock_adapter.py +++ b/tests/agent/test_bedrock_adapter.py @@ -1663,3 +1663,52 @@ class TestCallConverseStreamIamFallback: assert result.choices[0].message.content == "hi" # Not a stale connection — client stays cached. assert _bedrock_runtime_client_cache.get("us-east-1") is client + + +# --------------------------------------------------------------------------- +# boto3 version check +# --------------------------------------------------------------------------- + + +class TestRequireBoto3VersionCheck: + """Test that _require_boto3() rejects boto3 versions older than 1.34.59.""" + + def test_raises_runtime_error_when_boto3_too_old(self): + """boto3 < 1.34.59 should raise RuntimeError with upgrade instructions.""" + from agent.bedrock_adapter import _require_boto3 + + fake_boto3 = MagicMock() + fake_boto3.__version__ = "1.34.46" + with patch.dict("sys.modules", {"boto3": fake_boto3}): + with pytest.raises(RuntimeError, match="does not support converse_stream"): + _require_boto3() + + def test_accepts_boto3_at_minimum_version(self): + """boto3 == 1.34.59 should be accepted.""" + from agent.bedrock_adapter import _require_boto3 + + fake_boto3 = MagicMock() + fake_boto3.__version__ = "1.34.59" + with patch.dict("sys.modules", {"boto3": fake_boto3}): + result = _require_boto3() + assert result is fake_boto3 + + def test_accepts_newer_boto3(self): + """boto3 > 1.34.59 should be accepted.""" + from agent.bedrock_adapter import _require_boto3 + + fake_boto3 = MagicMock() + fake_boto3.__version__ = "1.42.89" + with patch.dict("sys.modules", {"boto3": fake_boto3}): + result = _require_boto3() + assert result is fake_boto3 + + def test_accepts_boto3_with_unparseable_version(self): + """If version string can't be parsed, don't block on version check.""" + from agent.bedrock_adapter import _require_boto3 + + fake_boto3 = MagicMock() + fake_boto3.__version__ = "dev" + with patch.dict("sys.modules", {"boto3": fake_boto3}): + result = _require_boto3() + assert result is fake_boto3