Skip to content

Commit 48ebd73

Browse files
kwnathantonpirker
andauthored
fix(anthropic): Add partial json support to streams (#3674)
Add `partial_json` for tool calling when streaming in Anthropic integrations. (This is an addition to #3615 --------- Co-authored-by: Anton Pirker <[email protected]>
1 parent 2423299 commit 48ebd73

File tree

2 files changed

+66
-7
lines changed

2 files changed

+66
-7
lines changed

Diff for: sentry_sdk/integrations/anthropic.py

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ def _collect_ai_data(event, input_tokens, output_tokens, content_blocks):
101101
elif event.type == "content_block_delta":
102102
if hasattr(event.delta, "text"):
103103
content_blocks.append(event.delta.text)
104+
elif hasattr(event.delta, "partial_json"):
105+
content_blocks.append(event.delta.partial_json)
104106
elif event.type == "content_block_stop":
105107
pass
106108
elif event.type == "message_delta":

Diff for: tests/integrations/anthropic/test_anthropic.py

+64-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from unittest import mock
22

3+
34
try:
45
from unittest.mock import AsyncMock
56
except ImportError:
@@ -10,7 +11,7 @@ async def __call__(self, *args, **kwargs):
1011

1112

1213
import pytest
13-
from anthropic import AsyncAnthropic, Anthropic, AnthropicError, AsyncStream, Stream
14+
from anthropic import Anthropic, AnthropicError, AsyncAnthropic, AsyncStream, Stream
1415
from anthropic.types import MessageDeltaUsage, TextDelta, Usage
1516
from anthropic.types.content_block_delta_event import ContentBlockDeltaEvent
1617
from anthropic.types.content_block_start_event import ContentBlockStartEvent
@@ -19,6 +20,7 @@ async def __call__(self, *args, **kwargs):
1920
from anthropic.types.message_delta_event import MessageDeltaEvent
2021
from anthropic.types.message_start_event import MessageStartEvent
2122

23+
from sentry_sdk.integrations.anthropic import _add_ai_data_to_span, _collect_ai_data
2224
from sentry_sdk.utils import package_version
2325

2426
try:
@@ -42,7 +44,7 @@ async def __call__(self, *args, **kwargs):
4244
except ImportError:
4345
from anthropic.types.content_block import ContentBlock as TextBlock
4446

45-
from sentry_sdk import start_transaction
47+
from sentry_sdk import start_transaction, start_span
4648
from sentry_sdk.consts import OP, SPANDATA
4749
from sentry_sdk.integrations.anthropic import AnthropicIntegration
4850

@@ -517,9 +519,8 @@ def test_streaming_create_message_with_input_json_delta(
517519
if send_default_pii and include_prompts:
518520
assert span["data"][SPANDATA.AI_INPUT_MESSAGES] == messages
519521
assert span["data"][SPANDATA.AI_RESPONSES] == [
520-
{"text": "", "type": "text"}
521-
] # we do not record InputJSONDelta because it could contain PII
522-
522+
{"text": "{'location': 'San Francisco, CA'}", "type": "text"}
523+
]
523524
else:
524525
assert SPANDATA.AI_INPUT_MESSAGES not in span["data"]
525526
assert SPANDATA.AI_RESPONSES not in span["data"]
@@ -654,8 +655,8 @@ async def test_streaming_create_message_with_input_json_delta_async(
654655
if send_default_pii and include_prompts:
655656
assert span["data"][SPANDATA.AI_INPUT_MESSAGES] == messages
656657
assert span["data"][SPANDATA.AI_RESPONSES] == [
657-
{"text": "", "type": "text"}
658-
] # we do not record InputJSONDelta because it could contain PII
658+
{"text": "{'location': 'San Francisco, CA'}", "type": "text"}
659+
]
659660

660661
else:
661662
assert SPANDATA.AI_INPUT_MESSAGES not in span["data"]
@@ -757,3 +758,59 @@ async def test_span_origin_async(sentry_init, capture_events):
757758

758759
assert event["contexts"]["trace"]["origin"] == "manual"
759760
assert event["spans"][0]["origin"] == "auto.ai.anthropic"
761+
762+
763+
@pytest.mark.skipif(
764+
ANTHROPIC_VERSION < (0, 27),
765+
reason="Versions <0.27.0 do not include InputJSONDelta.",
766+
)
767+
def test_collect_ai_data_with_input_json_delta():
768+
event = ContentBlockDeltaEvent(
769+
delta=InputJSONDelta(partial_json="test", type="input_json_delta"),
770+
index=0,
771+
type="content_block_delta",
772+
)
773+
774+
input_tokens = 10
775+
output_tokens = 20
776+
content_blocks = []
777+
778+
new_input_tokens, new_output_tokens, new_content_blocks = _collect_ai_data(
779+
event, input_tokens, output_tokens, content_blocks
780+
)
781+
782+
assert new_input_tokens == input_tokens
783+
assert new_output_tokens == output_tokens
784+
assert new_content_blocks == ["test"]
785+
786+
787+
@pytest.mark.skipif(
788+
ANTHROPIC_VERSION < (0, 27),
789+
reason="Versions <0.27.0 do not include InputJSONDelta.",
790+
)
791+
def test_add_ai_data_to_span_with_input_json_delta(sentry_init):
792+
sentry_init(
793+
integrations=[AnthropicIntegration(include_prompts=True)],
794+
traces_sample_rate=1.0,
795+
send_default_pii=True,
796+
)
797+
798+
with start_transaction(name="test"):
799+
span = start_span()
800+
integration = AnthropicIntegration()
801+
802+
_add_ai_data_to_span(
803+
span,
804+
integration,
805+
input_tokens=10,
806+
output_tokens=20,
807+
content_blocks=["{'test': 'data',", "'more': 'json'}"],
808+
)
809+
810+
assert span._data.get(SPANDATA.AI_RESPONSES) == [
811+
{"type": "text", "text": "{'test': 'data','more': 'json'}"}
812+
]
813+
assert span._data.get("ai.streaming") is True
814+
assert span._measurements.get("ai_prompt_tokens_used")["value"] == 10
815+
assert span._measurements.get("ai_completion_tokens_used")["value"] == 20
816+
assert span._measurements.get("ai_total_tokens_used")["value"] == 30

0 commit comments

Comments
 (0)