Skip to content
Merged
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
1 change: 1 addition & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh
- Dropped support for Litestar below 2.0.
- Dropped support for PySpark below 3.0.
- Dropped support for redis-py below 4.2.
- Dropped support for Pydantic AI below 1.76.
- Removed the RedisIntegration `max_data_size` option.
- Removed the possibility to supply a specific client to the LaunchDarklyIntegration.
- The `enable_tracing` option was removed. Use `traces_sample_rate=1.0` instead.
Expand Down
159 changes: 9 additions & 150 deletions scripts/populate_tox/package_dependencies.jsonl

Large diffs are not rendered by default.

236 changes: 3 additions & 233 deletions scripts/populate_tox/releases.jsonl

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion sentry_sdk/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def iter_default_integrations(
"openai": (1, 0, 0),
"openai_agents": (0, 0, 19),
"openfeature": (0, 7, 1),
"pydantic_ai": (1, 0, 0),
"pydantic_ai": (1, 76, 0),
"pymongo": (3, 5, 0),
"pyramid": (2, 0),
"pyreqwest": (0, 11, 6),
Expand Down
6 changes: 0 additions & 6 deletions sentry_sdk/integrations/pydantic_ai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

from .patches import (
_patch_agent_run,
_patch_graph_nodes,
_patch_tool_execution,
)
from .spans.ai_client import ai_client_span, update_ai_client_span
Expand Down Expand Up @@ -121,11 +120,6 @@ def setup_once() -> None:
_patch_agent_run()
_patch_tool_execution()

# ModelRequestContext.model added in https://github.com/pydantic/pydantic-ai/commit/f1260dfe09907f17688eee1646daf898fc428d4c
if PYDANTIC_AI_VERSION < (1, 73):
_patch_graph_nodes()
return

try:
from pydantic_ai.capabilities import Hooks
except ImportError:
Expand Down
1 change: 0 additions & 1 deletion sentry_sdk/integrations/pydantic_ai/patches/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
from .agent_run import _patch_agent_run # noqa: F401
from .graph_nodes import _patch_graph_nodes # noqa: F401
from .tools import _patch_tool_execution # noqa: F401
124 changes: 0 additions & 124 deletions sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py

This file was deleted.

83 changes: 0 additions & 83 deletions sentry_sdk/integrations/pydantic_ai/patches/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@ def _patch_tool_execution() -> None:
if hasattr(ToolManager, "execute_tool_call"):
_patch_execute_tool_call()

elif hasattr(ToolManager, "_call_tool"):
# older versions
_patch_call_tool()


def _patch_execute_tool_call() -> None:
original_execute_tool_call = ToolManager.execute_tool_call
Expand Down Expand Up @@ -102,82 +98,3 @@ async def wrapped_execute_tool_call(
return await original_execute_tool_call(self, validated, *args, **kwargs)

ToolManager.execute_tool_call = wrapped_execute_tool_call # type: ignore[method-assign]


def _patch_call_tool() -> None:
"""
Patch ToolManager._call_tool to create execute_tool spans.

This is the single point where ALL tool calls flow through in pydantic_ai,
regardless of toolset type (function, MCP, combined, wrapper, etc.).

By patching here, we avoid:
- Patching multiple toolset classes
- Dealing with signature mismatches from instrumented MCP servers
- Complex nested toolset handling
"""
original_call_tool = ToolManager._call_tool # type: ignore[attr-defined]

@wraps(original_call_tool)
async def wrapped_call_tool(
self: "Any", call: "Any", *args: "Any", **kwargs: "Any"
) -> "Any":
# 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)

# Get agent from contextvar
agent = get_current_agent()

if agent and tool:
try:
args_dict = call.args_as_dict()
except Exception:
args_dict = call.args if isinstance(call.args, dict) else {}

# Create execute_tool span
# Nesting is handled by isolation_scope() to ensure proper parent-child relationships
with sentry_sdk.isolation_scope():
with execute_tool_span(
name,
args_dict,
agent,
tool_definition=selected_tool_definition,
) as span:
try:
result = await original_call_tool(
self,
call,
*args,
**kwargs,
)
update_execute_tool_span(span, result)
return result
except ToolRetryError as exc:
exc_info = sys.exc_info()
with capture_internal_exceptions():
# Avoid circular import due to multi-file integration structure
from sentry_sdk.integrations.pydantic_ai import (
PydanticAIIntegration,
)

integration = sentry_sdk.get_client().get_integration(
PydanticAIIntegration
)
if (
integration is not None
and integration.handled_tool_call_exceptions
):
_capture_exception(exc, handled=True)
reraise(*exc_info)

# No span context - just call original
return await original_call_tool(
self,
call,
*args,
**kwargs,
)

ToolManager._call_tool = wrapped_call_tool # type: ignore[attr-defined]
9 changes: 7 additions & 2 deletions sentry_sdk/integrations/pydantic_ai/spans/ai_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
from typing import Any, Dict, List, Optional, Union

from pydantic_ai.messages import ModelMessage, ModelResponse, SystemPromptPart
from pydantic_ai.models import Model
from pydantic_ai.settings import ModelSettings

from sentry_sdk import _types

Expand Down Expand Up @@ -99,7 +101,7 @@ def _get_system_instructions(
return permanent_instructions, current_instructions


def _set_input_messages(span: "StreamedSpan", messages: "Any") -> None:
def _set_input_messages(span: "StreamedSpan", messages: "list[ModelMessage]") -> None:
"""Set input messages data on a span."""
if not _should_send_inputs():
return
Expand Down Expand Up @@ -268,7 +270,10 @@ def _set_output_data(


def ai_client_span(
messages: "Any", agent: "Any", model: "Any", model_settings: "Any"
messages: "list[ModelMessage]",
agent: "Any",
model: "Model",
model_settings: "Optional[ModelSettings]",
) -> "StreamedSpan":
"""Create a span for an AI client call (model request).

Expand Down
4 changes: 3 additions & 1 deletion sentry_sdk/integrations/pydantic_ai/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
if TYPE_CHECKING:
from typing import Any, Optional

from pydantic_ai.models import Model


# Store the current agent context in a contextvar for re-entrant safety
# Using a list as a stack to support nested agent calls
Expand Down Expand Up @@ -107,7 +109,7 @@ def _set_agent_data(span: "StreamedSpan", agent: "Any") -> None:
span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name)


def _get_model_name(model_obj: "Any") -> "Optional[str]":
def _get_model_name(model_obj: "Model") -> "Optional[str]":
"""Extract model name from a model object.

Args:
Expand Down
Loading
Loading