Skip to content

Commit 7cd10b9

Browse files
authored
feat: Support adding exception notes for Python 3.10 (#1034)
When add_note is not available (3.10) enhance the default error message with the added notes. In PR #290 we started using add_note to provide the bedrock model and region in exceptions to better clarify to customers what model & region were active. The implementation used add_note which is only supported in 3.11+; however, we've had enough customers on 3.10 where they're not seeing the error message that it makes sense to add a shim to do something similar for 3.10. --------- Co-authored-by: Mackenzie Zastrow <zastrowm@users.noreply.github.com>
1 parent 61e41da commit 7cd10b9

File tree

4 files changed

+115
-23
lines changed

4 files changed

+115
-23
lines changed

src/strands/_exception_notes.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Exception note utilities for Python 3.10+ compatibility."""
2+
3+
# add_note was added in 3.11 - we hoist to a constant to facilitate testing
4+
supports_add_note = hasattr(Exception, "add_note")
5+
6+
7+
def add_exception_note(exception: Exception, note: str) -> None:
8+
"""Add a note to an exception, compatible with Python 3.10+.
9+
10+
Uses add_note() if it's available (Python 3.11+) or modifies the exception message if it is not.
11+
"""
12+
if supports_add_note:
13+
# we ignore the mypy error because the version-check for add_note is extracted into a constant up above and
14+
# mypy doesn't detect that
15+
exception.add_note(note) # type: ignore
16+
else:
17+
# For Python 3.10, append note to the exception message
18+
if hasattr(exception, "args") and exception.args:
19+
exception.args = (f"{exception.args[0]}\n{note}",) + exception.args[1:]
20+
else:
21+
exception.args = (note,)

src/strands/models/bedrock.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from pydantic import BaseModel
1717
from typing_extensions import TypedDict, Unpack, override
1818

19+
from .._exception_notes import add_exception_note
1920
from ..event_loop import streaming
2021
from ..tools import convert_pydantic_to_tool_spec
2122
from ..types.content import ContentBlock, Messages
@@ -716,29 +717,29 @@ def _stream(
716717

717718
region = self.client.meta.region_name
718719

719-
# add_note added in Python 3.11
720-
if hasattr(e, "add_note"):
721-
# Aid in debugging by adding more information
722-
e.add_note(f"└ Bedrock region: {region}")
723-
e.add_note(f"└ Model id: {self.config.get('model_id')}")
724-
725-
if (
726-
e.response["Error"]["Code"] == "AccessDeniedException"
727-
and "You don't have access to the model" in error_message
728-
):
729-
e.add_note(
730-
"└ For more information see "
731-
"https://strandsagents.com/latest/user-guide/concepts/model-providers/amazon-bedrock/#model-access-issue"
732-
)
733-
734-
if (
735-
e.response["Error"]["Code"] == "ValidationException"
736-
and "with on-demand throughput isn’t supported" in error_message
737-
):
738-
e.add_note(
739-
"└ For more information see "
740-
"https://strandsagents.com/latest/user-guide/concepts/model-providers/amazon-bedrock/#on-demand-throughput-isnt-supported"
741-
)
720+
# Aid in debugging by adding more information
721+
add_exception_note(e, f"└ Bedrock region: {region}")
722+
add_exception_note(e, f"└ Model id: {self.config.get('model_id')}")
723+
724+
if (
725+
e.response["Error"]["Code"] == "AccessDeniedException"
726+
and "You don't have access to the model" in error_message
727+
):
728+
add_exception_note(
729+
e,
730+
"└ For more information see "
731+
"https://strandsagents.com/latest/user-guide/concepts/model-providers/amazon-bedrock/#model-access-issue",
732+
)
733+
734+
if (
735+
e.response["Error"]["Code"] == "ValidationException"
736+
and "with on-demand throughput isn’t supported" in error_message
737+
):
738+
add_exception_note(
739+
e,
740+
"└ For more information see "
741+
"https://strandsagents.com/latest/user-guide/concepts/model-providers/amazon-bedrock/#on-demand-throughput-isnt-supported",
742+
)
742743

743744
raise e
744745

tests/strands/models/test_bedrock.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import sys
3+
import traceback
34
import unittest.mock
45
from unittest.mock import ANY
56

@@ -10,6 +11,7 @@
1011
from botocore.exceptions import ClientError, EventStreamError
1112

1213
import strands
14+
from strands import _exception_notes
1315
from strands.models import BedrockModel
1416
from strands.models.bedrock import (
1517
_DEFAULT_BEDROCK_MODEL_ID,
@@ -1209,6 +1211,23 @@ async def test_add_note_on_client_error(bedrock_client, model, alist, messages):
12091211
assert err.value.__notes__ == ["└ Bedrock region: us-west-2", "└ Model id: m1"]
12101212

12111213

1214+
@pytest.mark.asyncio
1215+
async def test_add_note_on_client_error_without_add_notes(bedrock_client, model, alist, messages):
1216+
"""Test that when add_note is not used, the region & model are still included in the error output."""
1217+
with unittest.mock.patch.object(_exception_notes, "supports_add_note", False):
1218+
# Mock the client error response
1219+
error_response = {"Error": {"Code": "ValidationException", "Message": "Some error message"}}
1220+
bedrock_client.converse_stream.side_effect = ClientError(error_response, "ConversationStream")
1221+
1222+
# Call the stream method which should catch and add notes to the exception
1223+
with pytest.raises(ClientError) as err:
1224+
await alist(model.stream(messages))
1225+
1226+
error_str = "".join(traceback.format_exception(err.value))
1227+
assert "└ Bedrock region: us-west-2" in error_str
1228+
assert "└ Model id: m1" in error_str
1229+
1230+
12121231
@pytest.mark.asyncio
12131232
async def test_no_add_note_when_not_available(bedrock_client, model, alist, messages):
12141233
"""Verify that on any python version (even < 3.11 where add_note is not available, we get the right exception)."""
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Tests for exception note utilities."""
2+
3+
import sys
4+
import traceback
5+
import unittest.mock
6+
7+
import pytest
8+
9+
from strands import _exception_notes
10+
from strands._exception_notes import add_exception_note
11+
12+
13+
@pytest.mark.skipif(sys.version_info < (3, 11), reason="This test requires Python 3.11 or higher (need add_note)")
14+
def test_add_exception_note_python_311_plus():
15+
"""Test add_exception_note uses add_note in Python 3.11+."""
16+
exception = ValueError("original message")
17+
18+
add_exception_note(exception, "test note")
19+
20+
assert traceback.format_exception(exception) == ["ValueError: original message\n", "test note\n"]
21+
22+
23+
def test_add_exception_note_python_310():
24+
"""Test add_exception_note modifies args in Python 3.10."""
25+
with unittest.mock.patch.object(_exception_notes, "supports_add_note", False):
26+
exception = ValueError("original message")
27+
28+
add_exception_note(exception, "test note")
29+
30+
assert traceback.format_exception(exception) == ["ValueError: original message\ntest note\n"]
31+
32+
33+
def test_add_exception_note_python_310_no_args():
34+
"""Test add_exception_note handles exception with no args in Python 3.10."""
35+
with unittest.mock.patch.object(_exception_notes, "supports_add_note", False):
36+
exception = ValueError()
37+
exception.args = ()
38+
39+
add_exception_note(exception, "test note")
40+
41+
assert traceback.format_exception(exception) == ["ValueError: test note\n"]
42+
43+
44+
def test_add_exception_note_python_310_multiple_args():
45+
"""Test add_exception_note preserves additional args in Python 3.10."""
46+
with unittest.mock.patch.object(_exception_notes, "supports_add_note", False):
47+
exception = ValueError("original message", "second arg")
48+
49+
add_exception_note(exception, "test note")
50+
51+
assert traceback.format_exception(exception) == ["ValueError: ('original message\\ntest note', 'second arg')\n"]

0 commit comments

Comments
 (0)