-
Notifications
You must be signed in to change notification settings - Fork 584
fix(pydantic-ai): Adapt to missing ToolManager._call_tool
#5522
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9c306de
1a853da
d04b052
ab50d09
ccaa3fe
4f6be8c
7cf1cab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,85 @@ | |
|
|
||
|
|
||
| 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 | ||
|
Comment on lines
+39
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we could maybe dedupe but the patches may diverge in future as well so I'm okay with either
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd keep them separate so that we can someday get rid of the old one easily |
||
|
|
||
| @wraps(original_execute_tool_call) | ||
| async def wrapped_execute_tool_call( | ||
| self: "Any", validated: "Any", *args: "Any", **kwargs: "Any" | ||
| ) -> "Any": | ||
| if not validated or not hasattr(validated, "call"): | ||
| return await original_execute_tool_call(self, validated, *args, **kwargs) | ||
|
|
||
| # Extract tool info before calling original | ||
| call = validated.call | ||
| name = call.tool_name | ||
| tool = self.tools.get(name) if self.tools else None | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # Determine tool type by checking tool.toolset | ||
| tool_type = "function" | ||
| if tool and HAS_MCP and isinstance(tool.toolset, MCPServer): | ||
| tool_type = "mcp" | ||
|
|
||
| # 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_type=tool_type, | ||
| ) as span: | ||
| try: | ||
| result = await original_execute_tool_call( | ||
| self, | ||
| validated, | ||
| *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) | ||
|
|
||
| return await original_execute_tool_call(self, validated, *args, **kwargs) | ||
|
|
||
| ToolManager.execute_tool_call = wrapped_execute_tool_call | ||
|
|
||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are existing tests for tool spans like |
||
| def _patch_call_tool() -> None: | ||
| """ | ||
| Patch ToolManager._call_tool to create execute_tool spans. | ||
|
|
||
|
|
@@ -39,7 +118,6 @@ def _patch_tool_execution() -> None: | |
| - Dealing with signature mismatches from instrumented MCP servers | ||
| - Complex nested toolset handling | ||
| """ | ||
|
|
||
| original_call_tool = ToolManager._call_tool | ||
|
|
||
| @wraps(original_call_tool) | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrong patch chosen when both methods exist
Medium Severity
_patch_tool_executionprioritizes patchingToolManager.execute_tool_callwhenever it exists, even ifToolManager._call_toolalso exists. In any pydantic-ai version where_call_toolremains the actual execution choke point, this can silently skip instrumentation (noexecute_tool_span) or change which layer is wrapped.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We prefer
execute_tool_callbecause it's the new way of doing things, and only fall back to the old way (_call_tool) if not possible.