Skip to content

Commit bb891ea

Browse files
ref(pydantic-ai): Use tool hooks (#7447)
Replace monkey patches with `hooks.on.tool_validate_error()` and `hooks.on.tool_execute()`. This is a breaking change because `tool_validate_error` fires once per validation attempt, capturing an exception per retry. The previous `ToolManager` patch only captured one exception per attempted tool execution, regardless of how many times it was retried.
1 parent 4c4e94b commit bb891ea

5 files changed

Lines changed: 78 additions & 113 deletions

File tree

sentry_sdk/integrations/pydantic_ai/__init__.py

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import functools
2+
import sys
23

4+
import sentry_sdk
35
from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version
4-
from sentry_sdk.utils import parse_version
6+
from sentry_sdk.utils import capture_internal_exceptions, parse_version, reraise
7+
8+
from .spans import execute_tool_span, update_execute_tool_span
9+
from .utils import _capture_exception
510

611
try:
712
import pydantic_ai # noqa: F401
813
from pydantic_ai import Agent
14+
from pydantic_ai.exceptions import ToolRetryError
915
except ImportError:
1016
raise DidNotEnable("pydantic-ai not installed")
1117

@@ -15,15 +21,27 @@
1521

1622
from .patches import (
1723
_patch_agent_run,
18-
_patch_tool_execution,
1924
)
2025
from .spans.ai_client import ai_client_span, update_ai_client_span
2126

2227
if TYPE_CHECKING:
2328
from typing import Any
2429

25-
from pydantic_ai import ModelRequestContext, RunContext
26-
from pydantic_ai.capabilities import Hooks, WrapModelRequestHandler
30+
from pydantic import ValidationError
31+
from pydantic_ai import (
32+
ModelRequestContext,
33+
ModelRetry,
34+
RunContext,
35+
ToolCallPart,
36+
ToolDefinition,
37+
)
38+
from pydantic_ai.capabilities import (
39+
Hooks,
40+
RawToolArgs,
41+
ValidatedToolArgs,
42+
WrapModelRequestHandler,
43+
WrapToolExecuteHandler,
44+
)
2745
from pydantic_ai.messages import ModelResponse
2846

2947

@@ -50,6 +68,56 @@ async def on_model_request(
5068
update_ai_client_span(span, response)
5169
return response
5270

71+
@hooks.on.tool_validate_error
72+
async def sentry_on_tool_validate_error(
73+
ctx: "RunContext[Any]",
74+
*,
75+
call: "ToolCallPart",
76+
tool_def: "ToolDefinition",
77+
args: "RawToolArgs",
78+
error: "ValidationError | ModelRetry",
79+
) -> "ValidatedToolArgs":
80+
with capture_internal_exceptions():
81+
integration = sentry_sdk.get_client().get_integration(
82+
PydanticAIIntegration,
83+
)
84+
if integration is not None and integration.handled_tool_call_exceptions:
85+
_capture_exception(error, handled=True)
86+
87+
raise error
88+
89+
@hooks.on.tool_execute
90+
async def sentry_wrap_tool_execute(
91+
ctx: "RunContext[Any]",
92+
*,
93+
call: "ToolCallPart",
94+
tool_def: "ToolDefinition",
95+
args: "ValidatedToolArgs",
96+
handler: "WrapToolExecuteHandler",
97+
) -> "Any":
98+
with execute_tool_span(
99+
tool_name=call.tool_name,
100+
tool_args=args,
101+
agent=ctx.agent,
102+
tool_definition=tool_def,
103+
) as span:
104+
try:
105+
result = await handler(args)
106+
update_execute_tool_span(span, result)
107+
return result
108+
except ToolRetryError as exc:
109+
exc_info = sys.exc_info()
110+
with capture_internal_exceptions():
111+
integration = sentry_sdk.get_client().get_integration(
112+
PydanticAIIntegration,
113+
)
114+
if (
115+
integration is not None
116+
and integration.handled_tool_call_exceptions
117+
):
118+
_capture_exception(exc, handled=True)
119+
reraise(*exc_info)
120+
53121
original_init = Agent.__init__
54122

55123
@functools.wraps(original_init)
@@ -118,7 +186,6 @@ def setup_once() -> None:
118186
return
119187

120188
_patch_agent_run()
121-
_patch_tool_execution()
122189

123190
try:
124191
from pydantic_ai.capabilities import Hooks
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
from .agent_run import _patch_agent_run # noqa: F401
2-
from .tools import _patch_tool_execution # noqa: F401

sentry_sdk/integrations/pydantic_ai/patches/tools.py

Lines changed: 0 additions & 100 deletions
This file was deleted.

sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111
if TYPE_CHECKING:
1212
from typing import Any, Optional
1313

14+
from pydantic_ai import Agent
1415
from pydantic_ai._tool_manager import ToolDefinition # type: ignore
1516

1617

1718
def execute_tool_span(
1819
tool_name: str,
19-
tool_args: "Any",
20-
agent: "Any",
21-
tool_definition: "Optional[ToolDefinition]" = None,
20+
tool_args: "dict[str, Any]",
21+
agent: "Optional[Agent[Any, Any]]",
22+
tool_definition: "ToolDefinition",
2223
) -> "StreamedSpan":
2324
"""Create a span for tool execution.
2425

tests/integrations/pydantic_ai/test_pydantic_ai.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -549,10 +549,8 @@ def add_numbers(a: Annotated[int, Field(gt=0, lt=0)], b: int) -> int:
549549
assert result is None
550550

551551
if handled_tool_call_exceptions:
552-
(
553-
error,
554-
model_behaviour_error,
555-
) = (item.payload for item in items if item.type == "event")
552+
events = [item.payload for item in items if item.type == "event"]
553+
error = events[0]
556554

557555
assert error["level"] == "error"
558556
assert error["exception"]["values"][0]["mechanism"]["handled"]

0 commit comments

Comments
 (0)