Skip to content

Commit 4e4d69a

Browse files
authored
fix(pydantic-ai): Adapt to missing ToolManager._call_tool (#5522)
### Description The internal function `ToolManager._call_tool` was removed. Patch `ToolManager.execute_tool_call` instead. Also: - pull in a test matrix update so that we actually run the test suite for the version of Pydantic AI where the change was introduced - make two pydantic ai tests compatible with one older pydantic ai version that used to complain about specific message history orders #### Issues Fixes #5518 #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr)
1 parent c88d142 commit 4e4d69a

File tree

5 files changed

+132
-50
lines changed

5 files changed

+132
-50
lines changed

scripts/populate_tox/package_dependencies.jsonl

Lines changed: 7 additions & 6 deletions
Large diffs are not rendered by default.

scripts/populate_tox/releases.jsonl

Lines changed: 17 additions & 16 deletions
Large diffs are not rendered by default.

sentry_sdk/integrations/pydantic_ai/patches/tools.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,85 @@
2828

2929

3030
def _patch_tool_execution() -> None:
31+
if hasattr(ToolManager, "execute_tool_call"):
32+
_patch_execute_tool_call()
33+
34+
elif hasattr(ToolManager, "_call_tool"):
35+
# older versions
36+
_patch_call_tool()
37+
38+
39+
def _patch_execute_tool_call() -> None:
40+
original_execute_tool_call = ToolManager.execute_tool_call
41+
42+
@wraps(original_execute_tool_call)
43+
async def wrapped_execute_tool_call(
44+
self: "Any", validated: "Any", *args: "Any", **kwargs: "Any"
45+
) -> "Any":
46+
if not validated or not hasattr(validated, "call"):
47+
return await original_execute_tool_call(self, validated, *args, **kwargs)
48+
49+
# Extract tool info before calling original
50+
call = validated.call
51+
name = call.tool_name
52+
tool = self.tools.get(name) if self.tools else None
53+
54+
# Determine tool type by checking tool.toolset
55+
tool_type = "function"
56+
if tool and HAS_MCP and isinstance(tool.toolset, MCPServer):
57+
tool_type = "mcp"
58+
59+
# Get agent from contextvar
60+
agent = get_current_agent()
61+
62+
if agent and tool:
63+
try:
64+
args_dict = call.args_as_dict()
65+
except Exception:
66+
args_dict = call.args if isinstance(call.args, dict) else {}
67+
68+
# Create execute_tool span
69+
# Nesting is handled by isolation_scope() to ensure proper parent-child relationships
70+
with sentry_sdk.isolation_scope():
71+
with execute_tool_span(
72+
name,
73+
args_dict,
74+
agent,
75+
tool_type=tool_type,
76+
) as span:
77+
try:
78+
result = await original_execute_tool_call(
79+
self,
80+
validated,
81+
*args,
82+
**kwargs,
83+
)
84+
update_execute_tool_span(span, result)
85+
return result
86+
except ToolRetryError as exc:
87+
exc_info = sys.exc_info()
88+
with capture_internal_exceptions():
89+
# Avoid circular import due to multi-file integration structure
90+
from sentry_sdk.integrations.pydantic_ai import (
91+
PydanticAIIntegration,
92+
)
93+
94+
integration = sentry_sdk.get_client().get_integration(
95+
PydanticAIIntegration
96+
)
97+
if (
98+
integration is not None
99+
and integration.handled_tool_call_exceptions
100+
):
101+
_capture_exception(exc, handled=True)
102+
reraise(*exc_info)
103+
104+
return await original_execute_tool_call(self, validated, *args, **kwargs)
105+
106+
ToolManager.execute_tool_call = wrapped_execute_tool_call
107+
108+
109+
def _patch_call_tool() -> None:
31110
"""
32111
Patch ToolManager._call_tool to create execute_tool spans.
33112
@@ -39,7 +118,6 @@ def _patch_tool_execution() -> None:
39118
- Dealing with signature mismatches from instrumented MCP servers
40119
- Complex nested toolset handling
41120
"""
42-
43121
original_call_tool = ToolManager._call_tool
44122

45123
@wraps(original_call_tool)

tests/integrations/pydantic_ai/test_pydantic_ai.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,9 @@ async def test_message_history(sentry_init, capture_events):
711711
from pydantic_ai import messages
712712

713713
history = [
714-
messages.UserPromptPart(content="Hello, I'm Alice"),
714+
messages.ModelRequest(
715+
parts=[messages.UserPromptPart(content="Hello, I'm Alice")]
716+
),
715717
messages.ModelResponse(
716718
parts=[messages.TextPart(content="Hello Alice! How can I help you?")],
717719
model_name="test",
@@ -1493,7 +1495,7 @@ async def test_message_formatting_with_different_parts(sentry_init, capture_even
14931495

14941496
# Create message history with different part types
14951497
history = [
1496-
messages.UserPromptPart(content="Hello"),
1498+
messages.ModelRequest(parts=[messages.UserPromptPart(content="Hello")]),
14971499
messages.ModelResponse(
14981500
parts=[
14991501
messages.TextPart(content="Hi there!"),

tox.ini

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,12 @@ envlist =
7272
{py3.10,py3.11,py3.12}-openai_agents-v0.0.19
7373
{py3.10,py3.12,py3.13}-openai_agents-v0.3.3
7474
{py3.10,py3.13,py3.14,py3.14t}-openai_agents-v0.6.9
75-
{py3.10,py3.13,py3.14,py3.14t}-openai_agents-v0.9.3
75+
{py3.10,py3.13,py3.14,py3.14t}-openai_agents-v0.10.1
7676

7777
{py3.10,py3.12,py3.13}-pydantic_ai-v1.0.18
78-
{py3.10,py3.12,py3.13}-pydantic_ai-v1.20.0
79-
{py3.10,py3.12,py3.13}-pydantic_ai-v1.40.0
80-
{py3.10,py3.13,py3.14}-pydantic_ai-v1.62.0
78+
{py3.10,py3.12,py3.13}-pydantic_ai-v1.21.0
79+
{py3.10,py3.12,py3.13}-pydantic_ai-v1.42.0
80+
{py3.10,py3.13,py3.14}-pydantic_ai-v1.63.0
8181

8282

8383
# ~~~ AI Workflow ~~~
@@ -116,22 +116,22 @@ envlist =
116116
{py3.9,py3.12,py3.13}-litellm-v1.77.7
117117
{py3.9,py3.12,py3.13}-litellm-v1.78.7
118118
{py3.9,py3.12,py3.13}-litellm-v1.79.3
119-
{py3.9,py3.12,py3.13}-litellm-v1.81.14
119+
{py3.9,py3.12,py3.13}-litellm-v1.81.15
120120

121121
{py3.8,py3.11,py3.12}-openai-base-v1.0.1
122122
{py3.8,py3.12,py3.13}-openai-base-v1.109.1
123-
{py3.9,py3.13,py3.14,py3.14t}-openai-base-v2.21.0
123+
{py3.9,py3.13,py3.14,py3.14t}-openai-base-v2.23.0
124124

125125
{py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1
126126
{py3.8,py3.12,py3.13}-openai-notiktoken-v1.109.1
127-
{py3.9,py3.13,py3.14,py3.14t}-openai-notiktoken-v2.21.0
127+
{py3.9,py3.13,py3.14,py3.14t}-openai-notiktoken-v2.23.0
128128

129129

130130
# ~~~ Cloud ~~~
131131
{py3.6,py3.7}-boto3-v1.12.49
132132
{py3.6,py3.9,py3.10}-boto3-v1.21.46
133133
{py3.7,py3.11,py3.12}-boto3-v1.33.13
134-
{py3.9,py3.13,py3.14,py3.14t}-boto3-v1.42.54
134+
{py3.9,py3.13,py3.14,py3.14t}-boto3-v1.42.55
135135

136136
{py3.6,py3.7,py3.8}-chalice-v1.16.0
137137
{py3.9,py3.12,py3.13}-chalice-v1.32.0
@@ -191,7 +191,7 @@ envlist =
191191
{py3.8,py3.12,py3.13}-graphene-v3.4.3
192192

193193
{py3.8,py3.10,py3.11}-strawberry-v0.209.8
194-
{py3.10,py3.13,py3.14,py3.14t}-strawberry-v0.305.0
194+
{py3.10,py3.13,py3.14,py3.14t}-strawberry-v0.306.0
195195

196196

197197
# ~~~ Network ~~~
@@ -256,9 +256,9 @@ envlist =
256256
{py3.10,py3.13,py3.14,py3.14t}-starlette-v0.52.1
257257

258258
{py3.6,py3.9,py3.10}-fastapi-v0.79.1
259-
{py3.7,py3.10,py3.11}-fastapi-v0.96.1
260-
{py3.8,py3.11,py3.12}-fastapi-v0.113.0
261-
{py3.10,py3.13,py3.14,py3.14t}-fastapi-v0.131.0
259+
{py3.7,py3.10,py3.11}-fastapi-v0.97.0
260+
{py3.8,py3.12,py3.13}-fastapi-v0.115.14
261+
{py3.10,py3.13,py3.14,py3.14t}-fastapi-v0.133.0
262262

263263

264264
# ~~~ Web 2 ~~~
@@ -398,13 +398,13 @@ deps =
398398
openai_agents-v0.0.19: openai-agents==0.0.19
399399
openai_agents-v0.3.3: openai-agents==0.3.3
400400
openai_agents-v0.6.9: openai-agents==0.6.9
401-
openai_agents-v0.9.3: openai-agents==0.9.3
401+
openai_agents-v0.10.1: openai-agents==0.10.1
402402
openai_agents: pytest-asyncio
403403

404404
pydantic_ai-v1.0.18: pydantic-ai==1.0.18
405-
pydantic_ai-v1.20.0: pydantic-ai==1.20.0
406-
pydantic_ai-v1.40.0: pydantic-ai==1.40.0
407-
pydantic_ai-v1.62.0: pydantic-ai==1.62.0
405+
pydantic_ai-v1.21.0: pydantic-ai==1.21.0
406+
pydantic_ai-v1.42.0: pydantic-ai==1.42.0
407+
pydantic_ai-v1.63.0: pydantic-ai==1.63.0
408408
pydantic_ai: pytest-asyncio
409409

410410

@@ -463,18 +463,18 @@ deps =
463463
litellm-v1.77.7: litellm==1.77.7
464464
litellm-v1.78.7: litellm==1.78.7
465465
litellm-v1.79.3: litellm==1.79.3
466-
litellm-v1.81.14: litellm==1.81.14
466+
litellm-v1.81.15: litellm==1.81.15
467467

468468
openai-base-v1.0.1: openai==1.0.1
469469
openai-base-v1.109.1: openai==1.109.1
470-
openai-base-v2.21.0: openai==2.21.0
470+
openai-base-v2.23.0: openai==2.23.0
471471
openai-base: pytest-asyncio
472472
openai-base: tiktoken
473473
openai-base-v1.0.1: httpx<0.28
474474

475475
openai-notiktoken-v1.0.1: openai==1.0.1
476476
openai-notiktoken-v1.109.1: openai==1.109.1
477-
openai-notiktoken-v2.21.0: openai==2.21.0
477+
openai-notiktoken-v2.23.0: openai==2.23.0
478478
openai-notiktoken: pytest-asyncio
479479
openai-notiktoken-v1.0.1: httpx<0.28
480480

@@ -483,7 +483,7 @@ deps =
483483
boto3-v1.12.49: boto3==1.12.49
484484
boto3-v1.21.46: boto3==1.21.46
485485
boto3-v1.33.13: boto3==1.33.13
486-
boto3-v1.42.54: boto3==1.42.54
486+
boto3-v1.42.55: boto3==1.42.55
487487
{py3.7,py3.8}-boto3: urllib3<2.0.0
488488

489489
chalice-v1.16.0: chalice==1.16.0
@@ -562,7 +562,7 @@ deps =
562562
{py3.6}-graphene: aiocontextvars
563563

564564
strawberry-v0.209.8: strawberry-graphql[fastapi,flask]==0.209.8
565-
strawberry-v0.305.0: strawberry-graphql[fastapi,flask]==0.305.0
565+
strawberry-v0.306.0: strawberry-graphql[fastapi,flask]==0.306.0
566566
strawberry: httpx
567567
strawberry-v0.209.8: pydantic<2.11
568568

@@ -687,16 +687,16 @@ deps =
687687
{py3.6}-starlette: aiocontextvars
688688

689689
fastapi-v0.79.1: fastapi==0.79.1
690-
fastapi-v0.96.1: fastapi==0.96.1
691-
fastapi-v0.113.0: fastapi==0.113.0
692-
fastapi-v0.131.0: fastapi==0.131.0
690+
fastapi-v0.97.0: fastapi==0.97.0
691+
fastapi-v0.115.14: fastapi==0.115.14
692+
fastapi-v0.133.0: fastapi==0.133.0
693693
fastapi: httpx
694694
fastapi: pytest-asyncio
695695
fastapi: python-multipart
696696
fastapi: requests
697697
fastapi: anyio<4
698698
fastapi-v0.79.1: httpx<0.28.0
699-
fastapi-v0.96.1: httpx<0.28.0
699+
fastapi-v0.97.0: httpx<0.28.0
700700
{py3.6}-fastapi: aiocontextvars
701701

702702

0 commit comments

Comments
 (0)