Skip to content
4 changes: 4 additions & 0 deletions sentry_sdk/integrations/pydantic_ai/patches/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ async def wrapped_execute_tool_call(
call = validated.call
name = call.tool_name
tool = self.tools.get(name) if self.tools else None
selected_tool_definition = getattr(tool, "tool_def", None)

# Determine tool type by checking tool.toolset
tool_type = "function"
Expand All @@ -73,6 +74,7 @@ async def wrapped_execute_tool_call(
args_dict,
agent,
tool_type=tool_type,
tool_definition=selected_tool_definition,
) as span:
try:
result = await original_execute_tool_call(
Expand Down Expand Up @@ -127,6 +129,7 @@ async def wrapped_call_tool(
# Extract tool info before calling original
name = call.tool_name
tool = self.tools.get(name) if self.tools else None
selected_tool_definition = getattr(tool, "tool_def", None)

# Determine tool type by checking tool.toolset
tool_type = "function" # default
Expand All @@ -150,6 +153,7 @@ async def wrapped_call_tool(
args_dict,
agent,
tool_type=tool_type,
tool_definition=selected_tool_definition,
) as span:
try:
result = await original_call_tool(
Expand Down
14 changes: 13 additions & 1 deletion sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@

if TYPE_CHECKING:
from typing import Any, Optional
from pydantic_ai._tool_manager import ToolDefinition # type: ignore


def execute_tool_span(
tool_name: str, tool_args: "Any", agent: "Any", tool_type: str = "function"
tool_name: str,
tool_args: "Any",
agent: "Any",
tool_type: str = "function",
tool_definition: "Optional[ToolDefinition]" = None,
) -> "sentry_sdk.tracing.Span":
"""Create a span for tool execution.

Expand All @@ -21,6 +26,7 @@ def execute_tool_span(
tool_args: The arguments passed to the tool
agent: The agent executing the tool
tool_type: The type of tool ("function" for regular tools, "mcp" for MCP services)
tool_definition: The definition of the tool, if available
"""
span = sentry_sdk.start_span(
op=OP.GEN_AI_EXECUTE_TOOL,
Expand All @@ -32,6 +38,12 @@ def execute_tool_span(
span.set_data(SPANDATA.GEN_AI_TOOL_TYPE, tool_type)
span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name)

if tool_definition is not None:
span.set_data(
SPANDATA.GEN_AI_TOOL_DESCRIPTION,
getattr(tool_definition, "description", None),
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getattr on TypedDict silently returns None, losing description

High Severity

In pydantic-ai, ToolDefinition is a TypedDict, which at runtime is just a dict. Using getattr(tool_definition, "description", None) on a dict instance always returns None because dict keys are not attributes — it would need to be tool_definition.get("description") or tool_definition["description"] instead. This means the tool description feature is silently broken: the span data will always contain None for the description, even when a tool has a docstring.

Fix in Cursor Fix in Web


_set_agent_data(span, agent)

if _should_send_prompts() and tool_args is not None:
Expand Down
84 changes: 82 additions & 2 deletions tests/integrations/pydantic_ai/test_pydantic_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from pydantic_ai import Agent
from pydantic_ai.messages import BinaryContent, UserPromptPart
from pydantic_ai.usage import RequestUsage
from pydantic_ai.models.test import TestModel
from pydantic_ai.exceptions import ModelRetry, UnexpectedModelBehavior


Expand Down Expand Up @@ -2386,7 +2385,9 @@ async def test_execute_tool_span_with_mcp_type(sentry_init, capture_events):
Test execute_tool span with MCP tool type.
"""
import sentry_sdk
from sentry_sdk.integrations.pydantic_ai.spans.execute_tool import execute_tool_span
from sentry_sdk.integrations.pydantic_ai.spans.execute_tool import (
execute_tool_span,
)

sentry_init(
integrations=[PydanticAIIntegration()],
Expand Down Expand Up @@ -2794,3 +2795,82 @@ async def test_set_usage_data_with_cache_tokens(sentry_init, capture_events):
(span_data,) = event["spans"]
assert span_data["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 80
assert span_data["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 20


@pytest.mark.asyncio
async def test_tool_description_in_execute_tool_span(sentry_init, capture_events):
"""
Test that tool description from the tool's docstring is included in execute_tool spans.
"""
agent = Agent(
"test",
name="test_agent",
system_prompt="You are a helpful test assistant.",
)

@agent.tool_plain
def multiply_numbers(a: int, b: int) -> int:
"""Multiply two numbers and return the product."""
return a * b

sentry_init(
integrations=[PydanticAIIntegration()],
traces_sample_rate=1.0,
send_default_pii=True,
)

events = capture_events()

result = await agent.run("What is 5 times 3?")
assert result is not None

(transaction,) = events
spans = transaction["spans"]

tool_spans = [s for s in spans if s["op"] == "gen_ai.execute_tool"]
assert len(tool_spans) >= 1

tool_span = tool_spans[0]
assert tool_span["data"]["gen_ai.tool.name"] == "multiply_numbers"
assert SPANDATA.GEN_AI_TOOL_DESCRIPTION in tool_span["data"]
assert "Multiply two numbers" in tool_span["data"][SPANDATA.GEN_AI_TOOL_DESCRIPTION]


@pytest.mark.asyncio
async def test_tool_without_description_sets_tool_description_to_none(
sentry_init, capture_events
):
"""
Test that execute_tool spans set tool description to None when the tool has no docstring.
"""
agent = Agent(
"test",
name="test_agent",
system_prompt="You are a helpful test assistant.",
)

@agent.tool_plain
def no_docs_tool(a: int, b: int) -> int:
return a + b

sentry_init(
integrations=[PydanticAIIntegration()],
traces_sample_rate=1.0,
send_default_pii=True,
)

events = capture_events()

result = await agent.run("What is 5 + 3?")
assert result is not None

(transaction,) = events
spans = transaction["spans"]

tool_spans = [s for s in spans if s["op"] == "gen_ai.execute_tool"]
assert len(tool_spans) >= 1

tool_span = tool_spans[0]
assert tool_span["data"]["gen_ai.tool.name"] == "no_docs_tool"
assert SPANDATA.GEN_AI_TOOL_DESCRIPTION in tool_span["data"]
assert tool_span["data"][SPANDATA.GEN_AI_TOOL_DESCRIPTION] is None
Loading