Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,12 @@ class SPANDATA:
Example: [{"type": "text", "text": "You are a helpful assistant."},{"type": "text", "text": "Be concise and clear."}]
"""

GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions"
"""
The definitions of the tools available to the model.
Example: [{"name": "get_weather", "description": "Get the weather for a given location", "type": "function", "parameters": {"location": "string"}}]
"""

GEN_AI_REQUEST_MESSAGES = "gen_ai.request.messages"
"""
The messages passed to the model. The "content" can be a string or an array of objects.
Expand Down
31 changes: 31 additions & 0 deletions sentry_sdk/integrations/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
truncate_and_annotate_messages,
get_start_span_function,
transform_anthropic_content_part,
_normalize_data,
)
from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS
from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration
Expand Down Expand Up @@ -368,6 +369,29 @@ def _transform_system_instructions(
]


def _transform_anthropic_tools(
tools: "Iterable[ToolUnionParam]",
) -> "list[dict[str, Any]]":
transformed_tools = []
for tool in tools:
if not isinstance(tool, dict):
continue

transformed_tool = {
"name": tool.get("name"),
"description": tool.get("description"),
"type": "function",
}

input_schema = tool.get("input_schema")
if input_schema:
transformed_tool["parameters"] = _normalize_data(input_schema, unpack=False)

transformed_tools.append(transformed_tool)

return transformed_tools


def _set_common_input_data(
span: "Span",
integration: "AnthropicIntegration",
Expand Down Expand Up @@ -463,6 +487,13 @@ def _set_common_input_data(

if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore
span.set_data(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools))
if should_send_default_pii() and integration.include_prompts:
set_data_normalized(
span,
SPANDATA.GEN_AI_TOOL_DEFINITIONS,
_transform_anthropic_tools(tools),
unpack=False,
)


def _set_create_input_data(
Expand Down
108 changes: 108 additions & 0 deletions tests/integrations/anthropic/test_anthropic_tools_definitions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import pytest
from unittest import mock
import json

from anthropic import Anthropic
from anthropic.types.message import Message
from anthropic.types.usage import Usage

try:
from anthropic.types.text_block import TextBlock
except ImportError:
from anthropic.types.content_block import ContentBlock as TextBlock

from sentry_sdk import start_transaction
from sentry_sdk.consts import OP, SPANDATA
from sentry_sdk.integrations.anthropic import AnthropicIntegration


EXAMPLE_MESSAGE = Message(
id="msg_01XFDUDYJgAACzvnptvVoYEL",
model="model",
role="assistant",
content=[TextBlock(type="text", text="Hi, I'm Claude.")],
type="message",
stop_reason="end_turn",
usage=Usage(input_tokens=10, output_tokens=20),
)

TOOLS = [
{
"name": "get_weather",
"description": "Get the current weather in a given location",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
}
]


@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
def test_tool_definitions_in_create_message(
sentry_init, capture_events, send_default_pii, include_prompts
):
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)

messages = [
{
"role": "user",
"content": "What is the weather in San Francisco?",
}
]

with start_transaction(name="anthropic"):
client.messages.create(
max_tokens=1024,
messages=messages,
model="model",
tools=TOOLS,
)

assert len(events) == 1
(event,) = events
(span,) = event["spans"]

assert span["op"] == OP.GEN_AI_CHAT

# Check old available_tools attribute (always present if tools provided)
assert SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS in span["data"]
available_tools = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS])
assert available_tools == TOOLS

# Check new tool.definitions attribute (only present if PII and prompts enabled)
if send_default_pii and include_prompts:
assert SPANDATA.GEN_AI_TOOL_DEFINITIONS in span["data"]
tool_definitions = json.loads(span["data"][SPANDATA.GEN_AI_TOOL_DEFINITIONS])
assert len(tool_definitions) == 1
assert tool_definitions[0]["name"] == "get_weather"
assert (
tool_definitions[0]["description"]
== "Get the current weather in a given location"
)
assert tool_definitions[0]["type"] == "function"
assert tool_definitions[0]["parameters"] == TOOLS[0]["input_schema"]
else:
assert SPANDATA.GEN_AI_TOOL_DEFINITIONS not in span["data"]
Loading