Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
465aab0
test(pydantic-ai): Consolidate binary blob redaction tests to stop re…
alexander-alderman-webb Apr 7, 2026
f590e32
.
alexander-alderman-webb Apr 7, 2026
d95d78d
fix(pydantic-ai): Set system instructions after instructions are set …
alexander-alderman-webb Apr 7, 2026
de56c78
.
alexander-alderman-webb Apr 7, 2026
2bafd3c
.
alexander-alderman-webb Apr 7, 2026
2d03ece
.
alexander-alderman-webb Apr 7, 2026
371c1bc
add a streaming test
alexander-alderman-webb Apr 7, 2026
b771009
add pydantic-ai to linting requirements
alexander-alderman-webb Apr 7, 2026
13da396
re-add type ignore
alexander-alderman-webb Apr 7, 2026
49dc6f4
move type ignore
alexander-alderman-webb Apr 7, 2026
1f1cc44
add docstring
alexander-alderman-webb Apr 7, 2026
cb41f97
delay messages in stream path
alexander-alderman-webb Apr 7, 2026
3a04323
use hooks
alexander-alderman-webb Apr 7, 2026
6bd8586
type ignores
alexander-alderman-webb Apr 8, 2026
d65fdb1
remove test
alexander-alderman-webb Apr 8, 2026
0c4e443
.
alexander-alderman-webb Apr 8, 2026
ba53213
add annotations
alexander-alderman-webb Apr 8, 2026
1c4fb70
.
alexander-alderman-webb Apr 8, 2026
9a02158
restore image url tests
alexander-alderman-webb Apr 8, 2026
7cf1dc8
add error hook
alexander-alderman-webb Apr 8, 2026
b5b4d63
flip bool
alexander-alderman-webb Apr 8, 2026
d0cb35b
.
alexander-alderman-webb Apr 9, 2026
87bfb08
document
alexander-alderman-webb Apr 9, 2026
80c2ee6
document 2
alexander-alderman-webb Apr 9, 2026
f84ffae
linting
alexander-alderman-webb Apr 9, 2026
2fb567c
fix run sync test
alexander-alderman-webb Apr 9, 2026
1059be9
update
alexander-alderman-webb Apr 9, 2026
2606eef
use early return
alexander-alderman-webb Apr 9, 2026
3452e52
remove print from test
alexander-alderman-webb Apr 9, 2026
396f322
set metadata in agent.__init__
alexander-alderman-webb Apr 9, 2026
622a48f
docstring
alexander-alderman-webb Apr 9, 2026
b72be0f
check none instead of falsy
alexander-alderman-webb Apr 9, 2026
ef7802a
create metadata dict in agent run methods again
alexander-alderman-webb Apr 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
130 changes: 127 additions & 3 deletions sentry_sdk/integrations/pydantic_ai/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from sentry_sdk.integrations import DidNotEnable, Integration
import functools

from sentry_sdk.integrations import DidNotEnable, Integration
from sentry_sdk.utils import capture_internal_exceptions

try:
import pydantic_ai # type: ignore # noqa: F401
from pydantic_ai import Agent
except ImportError:
raise DidNotEnable("pydantic-ai not installed")

Expand All @@ -14,10 +17,119 @@
_patch_tool_execution,
)

from .spans.ai_client import ai_client_span, update_ai_client_span

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any
from pydantic_ai import ModelRequestContext, RunContext
from pydantic_ai.messages import ModelResponse # type: ignore
from pydantic_ai.capabilities import Hooks # type: ignore


def register_hooks(hooks: "Hooks") -> None:
"""
Creates hooks for chat model calls and register the hooks by adding the hooks to the `capabilities` argument passed to `Agent.__init__()`.
"""

@hooks.on.before_model_request # type: ignore
async def on_request(
ctx: "RunContext[None]", request_context: "ModelRequestContext"
) -> "ModelRequestContext":
run_context_metadata = ctx.metadata
if not isinstance(run_context_metadata, dict):
return request_context

span = ai_client_span(
messages=request_context.messages,
agent=None,
model=request_context.model,
model_settings=request_context.model_settings,
)

run_context_metadata["_sentry_span"] = span
span.__enter__()

return request_context

@hooks.on.after_model_request # type: ignore
async def on_response(
ctx: "RunContext[None]",
*,
request_context: "ModelRequestContext",
response: "ModelResponse",
) -> "ModelResponse":
run_context_metadata = ctx.metadata
if not isinstance(run_context_metadata, dict):
return response

span = run_context_metadata.pop("_sentry_span", None)
if span is None:
return response

update_ai_client_span(span, response)
span.__exit__(None, None, None)

return response

@hooks.on.model_request_error # type: ignore
async def on_error(
ctx: "RunContext[None]",
*,
request_context: "ModelRequestContext",
error: "Exception",
) -> "ModelResponse":
run_context_metadata = ctx.metadata

if not isinstance(run_context_metadata, dict):
raise error

span = run_context_metadata.pop("_sentry_span", None)
if span is None:
raise error

with capture_internal_exceptions():
span.__exit__(type(error), error, error.__traceback__)

raise error

original_init = Agent.__init__

@functools.wraps(original_init)
def patched_init(self: "Agent[Any, Any]", *args: "Any", **kwargs: "Any") -> None:
caps = list(kwargs.get("capabilities") or [])
caps.append(hooks)
kwargs["capabilities"] = caps

metadata = kwargs.get("metadata")
if metadata is None:
kwargs["metadata"] = {} # Used as shared reference between hooks

return original_init(self, *args, **kwargs)

Agent.__init__ = patched_init


class PydanticAIIntegration(Integration):
"""
Typical interaction with the library:
1. The user creates an Agent instance with configuration, including system instructions sent to every model call.
2. The user calls `Agent.run()` or `Agent.run_stream()` to start an agent run. The latter can be used to incrementally receive progress.
- Each run invocation has `RunContext` objects that are passed to the library hooks.
3. In a loop, the agent repeatedly calls the model, maintaining a conversation history that includes previous messages and tool results, which is passed to each call.

Internally, Pydantic AI maintains an execution graph in which ModelRequestNode are responsible for model calls, including retries.
Hooks using the decorators provided by `pydantic_ai.capabilities` create and manage spans for model calls when these hooks are available (newer library versions).
The span is created in `on_request` and stored in the metadata of the `RunContext` object shared with `on_response` and `on_error`.

The metadata dictionary on the RunContext instance is initialized with `{"_sentry_span": None}` in the `_create_run_wrapper()` and `_create_streaming_wrapper()` wrappers that
instrument `Agent.run()` and `Agent.run_stream()`, respectively. A non-empty dictionary is required for the metadata object to be a shared reference between hooks.
"""

identifier = "pydantic_ai"
origin = f"auto.ai.{identifier}"
are_request_hooks_available = True

def __init__(
self, include_prompts: bool = True, handled_tool_call_exceptions: bool = True
Expand Down Expand Up @@ -45,6 +157,18 @@ def setup_once() -> None:
- Tool executions
"""
_patch_agent_run()
_patch_graph_nodes()
_patch_model_request()
_patch_tool_execution()

try:
from pydantic_ai.capabilities import Hooks
except ImportError:
Hooks = None
PydanticAIIntegration.are_request_hooks_available = False

if Hooks is None:
_patch_graph_nodes()
_patch_model_request()
return

hooks = Hooks()
register_hooks(hooks)
16 changes: 16 additions & 0 deletions sentry_sdk/integrations/pydantic_ai/patches/agent_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ def _create_run_wrapper(
original_func: The original run method
is_streaming: Whether this is a streaming method (for future use)
"""
from sentry_sdk.integrations.pydantic_ai import (
PydanticAIIntegration,
) # Required to avoid circular import

@wraps(original_func)
async def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
Expand All @@ -107,6 +110,11 @@ async def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
model = kwargs.get("model")
model_settings = kwargs.get("model_settings")

if PydanticAIIntegration.are_request_hooks_available:
metadata = kwargs.get("metadata")
if not metadata:
kwargs["metadata"] = {"_sentry_span": None}

# Create invoke_agent span
with invoke_agent_span(
user_prompt, self, model, model_settings, is_streaming
Expand Down Expand Up @@ -140,6 +148,9 @@ def _create_streaming_wrapper(
"""
Wraps run_stream method that returns an async context manager.
"""
from sentry_sdk.integrations.pydantic_ai import (
PydanticAIIntegration,
) # Required to avoid circular import

@wraps(original_func)
def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
Expand All @@ -148,6 +159,11 @@ def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
model = kwargs.get("model")
model_settings = kwargs.get("model_settings")

if PydanticAIIntegration.are_request_hooks_available:
metadata = kwargs.get("metadata")
if not metadata:
kwargs["metadata"] = {"_sentry_span": None}

# Call original function to get the context manager
original_ctx_manager = original_func(self, *args, **kwargs)

Expand Down
Loading
Loading