Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
33 changes: 33 additions & 0 deletions sentry_sdk/integrations/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,32 @@ 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"),
"type": "function",
}

description = tool.get("description")
if description is not None:
transformed_tool["description"] = description

input_schema = tool.get("input_schema")
if input_schema is not None:
transformed_tool["parameters"] = input_schema

transformed_tools.append(transformed_tool)

return transformed_tools


def _set_common_input_data(
span: "Span",
integration: "AnthropicIntegration",
Expand Down Expand Up @@ -463,6 +489,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
123 changes: 123 additions & 0 deletions tests/integrations/anthropic/test_anthropic_tools_definitions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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"],
},
},
{
"name": "no_description_tool",
"input_schema": {
"type": "object",
"properties": {"arg1": {"type": "string"}},
},
},
]


@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) == 2

# Check tool with description
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"]

# Check tool without description
assert tool_definitions[1]["name"] == "no_description_tool"
assert "description" not in tool_definitions[1]
assert tool_definitions[1]["type"] == "function"
assert tool_definitions[1]["parameters"] == TOOLS[1]["input_schema"]
else:
assert SPANDATA.GEN_AI_TOOL_DEFINITIONS not in span["data"]
Loading