Skip to content

Commit 72a02b1

Browse files
feat(bedrock): Set gen_ai.system_instructions attribute (#8239)
Set system instructions from the `system` parameter passed to the Bedrock API.
1 parent 7c90371 commit 72a02b1

2 files changed

Lines changed: 196 additions & 1 deletion

File tree

‎sentry_sdk/integrations/boto3/_services/bedrock.py‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from sentry_sdk.consts import OP, SPANDATA
77
from sentry_sdk.integrations import DidNotEnable
88
from sentry_sdk.integrations.boto3._services.base import _ServiceExtension
9+
from sentry_sdk.scope import should_send_default_pii
910
from sentry_sdk.traces import SpanStatus, StreamedSpan
1011
from sentry_sdk.tracing import SPANSTATUS
1112
from sentry_sdk.utils import (
@@ -29,7 +30,7 @@
2930
ConverseStreamResponseTypeDef,
3031
)
3132

32-
from sentry_sdk._types import Attributes, ToolDefinition
33+
from sentry_sdk._types import Attributes, TextPart, ToolDefinition
3334
from sentry_sdk.integrations.boto3._context import AwsCallContext
3435
from sentry_sdk.tracing import Span
3536

@@ -146,6 +147,19 @@ def sentry_patched_close(self: "EventStream[Any]") -> None:
146147
EventStream.close = sentry_patched_close # type: ignore[method-assign]
147148

148149

150+
def _transform_system_instructions(
151+
system_instructions: "Union[list[dict[str, Any]], tuple[dict[str, Any], ...]]",
152+
) -> "list[TextPart]":
153+
instruction_text_parts: "list[TextPart]" = []
154+
for block in system_instructions:
155+
if not isinstance(block, dict) or "text" not in block:
156+
continue
157+
158+
instruction_text_parts.append({"type": "text", "content": block["text"]})
159+
160+
return instruction_text_parts
161+
162+
149163
def _transform_tool_definitions(
150164
tools: "list[dict[str, Any]]",
151165
) -> "list[ToolDefinition]":
@@ -236,6 +250,14 @@ def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes":
236250
if not client_options["data_collection"]["gen_ai"]["inputs"]:
237251
return attributes
238252

253+
system_instructions = ctx.params.get("system")
254+
if (
255+
has_data_collection_enabled(client_options) or should_send_default_pii()
256+
) and isinstance(system_instructions, (list, tuple)):
257+
attributes[SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS] = json.dumps(
258+
_transform_system_instructions(system_instructions)
259+
)
260+
239261
tool_config = ctx.params.get("toolConfig")
240262
if not isinstance(tool_config, dict):
241263
return attributes

‎tests/integrations/boto3/test_bedrock.py‎

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,89 @@ def test_converse_tool_definitions(
474474
)
475475

476476

477+
@pytest.mark.parametrize("data_collection", [True, False])
478+
@pytest.mark.parametrize("span_streaming", [True, False])
479+
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
480+
def test_converse_input_attributes(
481+
sentry_init,
482+
capture_items,
483+
capture_events,
484+
data_collection,
485+
span_streaming,
486+
stream_gen_ai_spans,
487+
converse_response,
488+
):
489+
if data_collection:
490+
sentry_init(
491+
traces_sample_rate=1.0,
492+
integrations=[Boto3Integration()],
493+
data_collection={"gen_ai": {"inputs": True}},
494+
stream_gen_ai_spans=stream_gen_ai_spans,
495+
trace_lifecycle="stream" if span_streaming else "static",
496+
)
497+
else:
498+
sentry_init(
499+
traces_sample_rate=1.0,
500+
integrations=[Boto3Integration()],
501+
send_default_pii=True,
502+
stream_gen_ai_spans=stream_gen_ai_spans,
503+
trace_lifecycle="stream" if span_streaming else "static",
504+
)
505+
506+
session = boto3.Session(
507+
aws_access_key_id="-", aws_secret_access_key="-", region_name="us-east-1"
508+
)
509+
client = session.client("bedrock-runtime")
510+
stubber = Stubber(client)
511+
stubber.add_response("converse", converse_response)
512+
513+
if span_streaming or stream_gen_ai_spans:
514+
items = capture_items("span")
515+
516+
with stubber, sentry_sdk.start_transaction(name="parent"):
517+
client.converse(
518+
modelId="anthropic.claude-3-sonnet-20240229-v1:0",
519+
messages=[{"role": "user", "content": [{"text": "Hi"}]}],
520+
system=[
521+
{"text": "You are a helpful assistant."},
522+
{"text": "Be concise and clear."},
523+
],
524+
)
525+
526+
sentry_sdk.flush()
527+
spans = [item.payload for item in items]
528+
(span,) = (
529+
span
530+
for span in spans
531+
if span["attributes"].get("sentry.op") == OP.GEN_AI_CHAT
532+
)
533+
534+
assert json.loads(span["attributes"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) == [
535+
{"type": "text", "content": "You are a helpful assistant."},
536+
{"type": "text", "content": "Be concise and clear."},
537+
]
538+
else:
539+
events = capture_events()
540+
541+
with stubber, sentry_sdk.start_transaction(name="parent"):
542+
client.converse(
543+
modelId="anthropic.claude-3-sonnet-20240229-v1:0",
544+
messages=[{"role": "user", "content": [{"text": "Hi"}]}],
545+
system=[
546+
{"text": "You are a helpful assistant."},
547+
{"text": "Be concise and clear."},
548+
],
549+
)
550+
551+
(transaction,) = events
552+
(span,) = transaction["spans"]
553+
554+
assert json.loads(span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) == [
555+
{"type": "text", "content": "You are a helpful assistant."},
556+
{"type": "text", "content": "Be concise and clear."},
557+
]
558+
559+
477560
@pytest.mark.parametrize("span_streaming", [True, False])
478561
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
479562
def test_converse_stream(
@@ -765,3 +848,93 @@ def test_converse_stream_tool_definitions(
765848
json.loads(span["data"][SPANDATA.GEN_AI_TOOL_DEFINITIONS])
766849
== expected_tool_definitions
767850
)
851+
852+
853+
@pytest.mark.parametrize("data_collection", [True, False])
854+
@pytest.mark.parametrize("span_streaming", [True, False])
855+
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
856+
def test_converse_stream_input_attributes(
857+
sentry_init,
858+
capture_items,
859+
capture_events,
860+
data_collection,
861+
span_streaming,
862+
stream_gen_ai_spans,
863+
converse_stream_response,
864+
):
865+
if data_collection:
866+
sentry_init(
867+
traces_sample_rate=1.0,
868+
integrations=[Boto3Integration()],
869+
data_collection={"gen_ai": {"inputs": True}},
870+
stream_gen_ai_spans=stream_gen_ai_spans,
871+
trace_lifecycle="stream" if span_streaming else "static",
872+
)
873+
else:
874+
sentry_init(
875+
traces_sample_rate=1.0,
876+
integrations=[Boto3Integration()],
877+
send_default_pii=True,
878+
stream_gen_ai_spans=stream_gen_ai_spans,
879+
trace_lifecycle="stream" if span_streaming else "static",
880+
)
881+
882+
session = boto3.Session(
883+
aws_access_key_id="-", aws_secret_access_key="-", region_name="us-east-1"
884+
)
885+
client = session.client("bedrock-runtime")
886+
mock_http = Mock(status_code=200)
887+
client.meta.events.register(
888+
"before-call.bedrock-runtime.ConverseStream",
889+
lambda **kwargs: (mock_http, converse_stream_response),
890+
)
891+
892+
if span_streaming or stream_gen_ai_spans:
893+
items = capture_items("span")
894+
895+
with sentry_sdk.start_transaction(name="parent"):
896+
response = client.converse_stream(
897+
modelId="anthropic.claude-3-sonnet-20240229-v1:0",
898+
messages=[{"role": "user", "content": [{"text": "Hi"}]}],
899+
system=[
900+
{"text": "You are a helpful assistant."},
901+
{"text": "Be concise and clear."},
902+
],
903+
)
904+
for _ in response["stream"]:
905+
pass
906+
907+
sentry_sdk.flush()
908+
spans = [item.payload for item in items]
909+
(span,) = (
910+
span
911+
for span in spans
912+
if span["attributes"].get("sentry.op") == OP.GEN_AI_CHAT
913+
)
914+
915+
assert json.loads(span["attributes"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) == [
916+
{"type": "text", "content": "You are a helpful assistant."},
917+
{"type": "text", "content": "Be concise and clear."},
918+
]
919+
else:
920+
events = capture_events()
921+
922+
with sentry_sdk.start_transaction(name="parent"):
923+
response = client.converse_stream(
924+
modelId="anthropic.claude-3-sonnet-20240229-v1:0",
925+
messages=[{"role": "user", "content": [{"text": "Hi"}]}],
926+
system=[
927+
{"text": "You are a helpful assistant."},
928+
{"text": "Be concise and clear."},
929+
],
930+
)
931+
for _ in response["stream"]:
932+
pass
933+
934+
(transaction,) = events
935+
(span,) = transaction["spans"]
936+
937+
assert json.loads(span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) == [
938+
{"type": "text", "content": "You are a helpful assistant."},
939+
{"type": "text", "content": "Be concise and clear."},
940+
]

0 commit comments

Comments
 (0)