Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d246712
feat(pydantic-ai): Add tool description to execute_tool spans
ericapisani Mar 5, 2026
b07537a
Merge branch 'master' into ep/add-tool-description-to-pydantic-ai-spans
ericapisani Mar 6, 2026
a136913
ref(pydantic-ai): Fix type annotation style in execute_tool_span
ericapisani Mar 6, 2026
352c03d
Merge branch 'ep/add-tool-description-to-pydantic-ai-spans' of github…
ericapisani Mar 6, 2026
a0bd0dc
test(pydantic-ai): Fix assertion for missing tool description
ericapisani Mar 6, 2026
b80dd97
fix(pydantic-ai): Safely access tool description attribute
ericapisani Mar 6, 2026
46743ae
Address CR comments
ericapisani Mar 6, 2026
bf85285
cleanups
ericapisani Mar 6, 2026
7a7f2bc
changes to make warden happy
ericapisani Mar 6, 2026
aee6675
test(pydantic-ai): Update test to verify tool description is omitted …
ericapisani Mar 9, 2026
2b7dca9
Claude forgot to include the functionality that required the test cha…
ericapisani Mar 9, 2026
3c9206d
Merge branch 'master' into ep/add-tool-description-to-pydantic-ai-spans
ericapisani Mar 9, 2026
2ae0c03
Switching to geetattr
ericapisani Mar 9, 2026
d204945
test(openai-agents): Remove test for unreachable state (#5584)
alexander-alderman-webb Mar 9, 2026
c2ce8bc
ref: Add experimental streaming API (5) (#5592)
sentrivana Mar 9, 2026
8a8427f
ref: Add new_trace, continue_trace to span first (6) (#5593)
sentrivana Mar 9, 2026
8ffcab9
test(openai-agents): Replace mocks with `httpx` in non-error single-r…
alexander-alderman-webb Mar 9, 2026
e8120f8
ref: Add streaming trace decorator (7) (#5594)
sentrivana Mar 9, 2026
f50264f
ci: Do not run actions on `potel-base` (#5614)
sentrivana Mar 9, 2026
f55c183
test(openai-agents): Replace mocks with `httpx` in API error test (#5…
alexander-alderman-webb Mar 9, 2026
ae8c320
test(openai-agents): Replace mocks with `httpx` in handoff tests (#5604)
alexander-alderman-webb Mar 9, 2026
056fc7c
test(openai-agents): Replace mocks with `httpx` in MCP tool tests (#5…
alexander-alderman-webb Mar 9, 2026
5a0e853
cleanup
ericapisani Mar 9, 2026
2f2db4c
Merge branch 'ep/add-tool-description-to-pydantic-ai-spans' of github…
ericapisani Mar 9, 2026
9f71684
Merge branch 'master' into ep/add-tool-description-to-pydantic-ai-spans
ericapisani Mar 9, 2026
597bb31
Remove unnecessary test
ericapisani Mar 9, 2026
241ba49
Merge branch 'ep/add-tool-description-to-pydantic-ai-spans' of github…
ericapisani Mar 9, 2026
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
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
11 changes: 10 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 @@
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,9 @@
span.set_data(SPANDATA.GEN_AI_TOOL_TYPE, tool_type)
span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name)

if tool_definition:
span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_definition.description)

Check warning on line 42 in sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py

View workflow job for this annotation

GitHub Actions / warden: find-bugs

[2AX-BK8] Test asserts tool description is present but None, contradicting PR's stated omission behavior (additional location)

The test `test_tool_without_description_omits_tool_description` (line 2871-2872) asserts that when a tool has no docstring, `SPANDATA.GEN_AI_TOOL_DESCRIPTION` should be present in the span data with value `None`. However, the PR description explicitly states: 'If no description is available (e.g. the tool has no docstring), the attribute is omitted from the span entirely.' The implementation in `execute_tool.py` line 41-42 only checks `if tool_definition:` before setting the description, so it will set `None` when the tool exists but has no docstring. The test should either assert the attribute is NOT present (matching PR intent), or the implementation needs to check `if tool_definition and tool_definition.description:`.

_set_agent_data(span, agent)

if _should_send_prompts() and tool_args is not None:
Expand Down
143 changes: 142 additions & 1 deletion tests/integrations/pydantic_ai/test_pydantic_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
from sentry_sdk.integrations.pydantic_ai import PydanticAIIntegration
from sentry_sdk.integrations.pydantic_ai.spans.ai_client import _set_input_messages
from sentry_sdk.integrations.pydantic_ai.spans.utils import _set_usage_data
from sentry_sdk.integrations.pydantic_ai.spans.execute_tool import execute_tool_span

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
from pydantic_ai._tool_manager import ToolDefinition


@pytest.fixture
Expand Down Expand Up @@ -2794,3 +2795,143 @@
(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_omits_tool_description(
sentry_init, capture_events
):
"""
Test that execute_tool spans omit tool description 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

Check warning on line 2871 in tests/integrations/pydantic_ai/test_pydantic_ai.py

View workflow job for this annotation

GitHub Actions / warden: code-review

Test asserts tool description is present when PR description says it should be omitted

The test `test_tool_without_description_omits_tool_description` asserts that `SPANDATA.GEN_AI_TOOL_DESCRIPTION in tool_span['data']` is True and its value is None. However, the PR description explicitly states: 'If no description is available (e.g. the tool has no docstring), the attribute is omitted from the span entirely.' The test docstring also says 'omits tool description' but the assertions verify the opposite - that it IS present with a None value.

Check warning on line 2872 in tests/integrations/pydantic_ai/test_pydantic_ai.py

View workflow job for this annotation

GitHub Actions / warden: find-bugs

Test asserts tool description is present but None, contradicting PR's stated omission behavior

The test `test_tool_without_description_omits_tool_description` (line 2871-2872) asserts that when a tool has no docstring, `SPANDATA.GEN_AI_TOOL_DESCRIPTION` should be present in the span data with value `None`. However, the PR description explicitly states: 'If no description is available (e.g. the tool has no docstring), the attribute is omitted from the span entirely.' The implementation in `execute_tool.py` line 41-42 only checks `if tool_definition:` before setting the description, so it will set `None` when the tool exists but has no docstring. The test should either assert the attribute is NOT present (matching PR intent), or the implementation needs to check `if tool_definition and tool_definition.description:`.
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


@pytest.mark.asyncio
async def test_execute_tool_span_with_tool_definition(sentry_init, capture_events):
"""
Test execute_tool_span helper correctly sets tool description from a ToolDefinition.
"""

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

events = capture_events()

tool_def = ToolDefinition(
name="my_tool",
description="A tool that does something useful.",
parameters_json_schema={"type": "object", "properties": {}},
)

with sentry_sdk.start_transaction(op="test", name="test"):
with execute_tool_span(
"my_tool", {"arg": "value"}, None, "function", tool_definition=tool_def
) as span:
assert span is not None

(event,) = events
tool_spans = [s for s in event["spans"] if s["op"] == "gen_ai.execute_tool"]
assert len(tool_spans) == 1
assert (
tool_spans[0]["data"][SPANDATA.GEN_AI_TOOL_DESCRIPTION]
== "A tool that does something useful."
)


@pytest.mark.asyncio
async def test_execute_tool_span_without_tool_definition(sentry_init, capture_events):
"""
Test execute_tool_span helper omits tool description when tool_definition is None.
"""

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

events = capture_events()

with sentry_sdk.start_transaction(op="test", name="test"):
with execute_tool_span(
"my_tool", {"arg": "value"}, None, "function", tool_definition=None
) as span:
assert span is not None

(event,) = events
tool_spans = [s for s in event["spans"] if s["op"] == "gen_ai.execute_tool"]
assert len(tool_spans) == 1
assert SPANDATA.GEN_AI_TOOL_DESCRIPTION not in tool_spans[0]["data"]
Loading