Skip to content
Open
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 pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ typing = [
"starlette>=1.3.1",
"python-multipart>=0.0.32",
"pydantic>=2.13.4",
"pydantic-ai-slim>=2.23.0",
]
test = [
"dataclasses ; python_full_version < '3.7'",
Expand Down
14 changes: 7 additions & 7 deletions sentry_sdk/integrations/pydantic_ai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from sentry_sdk.utils import capture_internal_exceptions, parse_version

try:
import pydantic_ai # type: ignore # noqa: F401
import pydantic_ai # noqa: F401
from pydantic_ai import Agent
except ImportError:
raise DidNotEnable("pydantic-ai not installed")
Expand All @@ -24,16 +24,16 @@
from typing import Any

from pydantic_ai import ModelRequestContext, RunContext
from pydantic_ai.capabilities import Hooks # type: ignore
from pydantic_ai.messages import ModelResponse # type: ignore
from pydantic_ai.capabilities import Hooks
from pydantic_ai.messages import ModelResponse


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
@hooks.on.before_model_request
async def on_request(
ctx: "RunContext[None]", request_context: "ModelRequestContext"
) -> "ModelRequestContext":
Expand All @@ -53,7 +53,7 @@ async def on_request(

return request_context

@hooks.on.after_model_request # type: ignore
@hooks.on.after_model_request
async def on_response(
ctx: "RunContext[None]",
*,
Expand All @@ -73,7 +73,7 @@ async def on_response(

return response

@hooks.on.model_request_error # type: ignore
@hooks.on.model_request_error
async def on_error(
ctx: "RunContext[None]",
*,
Expand Down Expand Up @@ -108,7 +108,7 @@ def patched_init(self: "Agent[Any, Any]", *args: "Any", **kwargs: "Any") -> None

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

Agent.__init__ = patched_init
Agent.__init__ = patched_init # type: ignore[method-assign]


class PydanticAIIntegration(Integration):
Expand Down
6 changes: 3 additions & 3 deletions sentry_sdk/integrations/pydantic_ai/patches/agent_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from ..utils import _capture_exception, pop_agent, push_agent

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

Expand Down Expand Up @@ -192,7 +192,7 @@ def _patch_agent_run() -> None:
original_run_stream = Agent.run_stream

# Wrap and apply patches for non-streaming methods
Agent.run = _create_run_wrapper(original_run, is_streaming=False)
Agent.run = _create_run_wrapper(original_run, is_streaming=False) # type: ignore[method-assign]

# Wrap and apply patches for streaming methods
Agent.run_stream = _create_streaming_wrapper(original_run_stream)
Agent.run_stream = _create_streaming_wrapper(original_run_stream) # type: ignore[method-assign]
6 changes: 3 additions & 3 deletions sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
)

try:
from pydantic_ai._agent_graph import ModelRequestNode # type: ignore
from pydantic_ai._agent_graph import ModelRequestNode
except ImportError:
raise DidNotEnable("pydantic-ai not installed")

Expand Down Expand Up @@ -70,7 +70,7 @@ async def wrapped_model_request_run(self: "Any", ctx: "Any") -> "Any":
update_ai_client_span(span, model_response)
return result

ModelRequestNode.run = wrapped_model_request_run
ModelRequestNode.run = wrapped_model_request_run # type: ignore[method-assign]

# Patch ModelRequestNode.stream for streaming requests
original_model_request_stream = ModelRequestNode.stream
Expand Down Expand Up @@ -103,4 +103,4 @@ async def wrapped_model_request_stream(self: "Any", ctx: "Any") -> "Any":

return wrapped_model_request_stream

ModelRequestNode.stream = create_wrapped_stream(original_model_request_stream)
ModelRequestNode.stream = create_wrapped_stream(original_model_request_stream) # type: ignore[method-assign]
10 changes: 5 additions & 5 deletions sentry_sdk/integrations/pydantic_ai/patches/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@

try:
try:
from pydantic_ai.tool_manager import ToolManager # type: ignore
from pydantic_ai.tool_manager import ToolManager
except ImportError:
from pydantic_ai._tool_manager import ToolManager # type: ignore

from pydantic_ai.exceptions import ToolRetryError # type: ignore
from pydantic_ai.exceptions import ToolRetryError
except ImportError:
raise DidNotEnable("pydantic-ai not installed")

Expand Down Expand Up @@ -95,7 +95,7 @@ async def wrapped_execute_tool_call(

return await original_execute_tool_call(self, validated, *args, **kwargs)

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


def _patch_call_tool() -> None:
Expand All @@ -110,7 +110,7 @@ def _patch_call_tool() -> None:
- Dealing with signature mismatches from instrumented MCP servers
- Complex nested toolset handling
"""
original_call_tool = ToolManager._call_tool
original_call_tool = ToolManager._call_tool # type: ignore[attr-defined]

@wraps(original_call_tool)
async def wrapped_call_tool(
Expand Down Expand Up @@ -174,4 +174,4 @@ async def wrapped_call_tool(
**kwargs,
)

ToolManager._call_tool = wrapped_call_tool
ToolManager._call_tool = wrapped_call_tool # type: ignore[attr-defined]
63 changes: 42 additions & 21 deletions sentry_sdk/integrations/pydantic_ai/spans/ai_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
if TYPE_CHECKING:
from typing import Any, Dict, List, Union

from pydantic_ai.messages import ModelMessage, SystemPromptPart # type: ignore
from pydantic_ai.messages import ModelMessage, SystemPromptPart

from sentry_sdk._types import TextPart as SentryTextPart

Expand All @@ -51,14 +51,14 @@
)
except ImportError:
# Fallback if these classes are not available
BaseToolCallPart = None
BaseToolReturnPart = None
SystemPromptPart = None
UserPromptPart = None
TextPart = None
ThinkingPart = None
BinaryContent = None
ImageUrl = None
BaseToolCallPart = None # type: ignore[misc,assignment]
BaseToolReturnPart = None # type: ignore[misc,assignment]
SystemPromptPart = None # type: ignore[misc,assignment]
UserPromptPart = None # type: ignore[misc,assignment]
TextPart = None # type: ignore[misc,assignment]
ThinkingPart = None # type: ignore[misc,assignment]
BinaryContent = None # type: ignore[misc,assignment]
ImageUrl = None # type: ignore[misc,assignment]


def _transform_system_instructions(
Expand Down Expand Up @@ -93,7 +93,7 @@ def _get_system_instructions(
for msg in messages:
if hasattr(msg, "parts"):
for part in msg.parts:
if SystemPromptPart and isinstance(part, SystemPromptPart):
if SystemPromptPart is not None and isinstance(part, SystemPromptPart):
permanent_instructions.append(part)

if hasattr(msg, "instructions") and msg.instructions is not None:
Expand Down Expand Up @@ -141,23 +141,32 @@ def _set_input_messages(
for part in msg.parts:
role = "user"
# Use isinstance checks with proper base classes
if SystemPromptPart and isinstance(part, SystemPromptPart):
if SystemPromptPart is not None and isinstance(
part, SystemPromptPart
):
continue
elif (
(TextPart and isinstance(part, TextPart))
or (ThinkingPart and isinstance(part, ThinkingPart))
or (BaseToolCallPart and isinstance(part, BaseToolCallPart))
(TextPart is not None and isinstance(part, TextPart))
or (ThinkingPart is not None and isinstance(part, ThinkingPart))
or (
BaseToolCallPart is not None
and isinstance(part, BaseToolCallPart)
)
):
role = "assistant"
elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart):
elif BaseToolReturnPart is not None and isinstance(
part, BaseToolReturnPart
):
role = "tool"

content: "List[Dict[str, Any] | str]" = []
tool_calls = None
tool_call_id = None

# Handle ToolCallPart (assistant requesting tool use)
if BaseToolCallPart and isinstance(part, BaseToolCallPart):
if BaseToolCallPart is not None and isinstance(
part, BaseToolCallPart
):
tool_call_data = {}
if hasattr(part, "tool_name"):
tool_call_data["name"] = part.tool_name
Expand All @@ -166,7 +175,9 @@ def _set_input_messages(
if tool_call_data:
tool_calls = [tool_call_data]
# Handle ToolReturnPart (tool result)
elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart):
elif BaseToolReturnPart is not None and isinstance(
part, BaseToolReturnPart
):
if hasattr(part, "tool_name"):
tool_call_id = part.tool_name
if hasattr(part, "content"):
Expand All @@ -179,9 +190,13 @@ def _set_input_messages(
for item in part.content:
if isinstance(item, str):
content.append({"type": "text", "text": item})
elif ImageUrl and isinstance(item, ImageUrl):
elif ImageUrl is not None and isinstance(
item, ImageUrl
):
content.append(_serialize_image_url_item(item))
elif BinaryContent and isinstance(item, BinaryContent):
elif BinaryContent is not None and isinstance(
item, BinaryContent
):
content.append(_serialize_binary_content_item(item))
else:
content.append(safe_serialize(item))
Expand Down Expand Up @@ -237,9 +252,15 @@ def _set_output_data(
tool_calls = []

for part in response.parts:
if TextPart and isinstance(part, TextPart) and hasattr(part, "content"):
if (
TextPart is not None
and isinstance(part, TextPart)
and hasattr(part, "content")
):
texts.append(part.content)
elif BaseToolCallPart and isinstance(part, BaseToolCallPart):
elif BaseToolCallPart is not None and isinstance(
part, BaseToolCallPart
):
tool_call_data = {
"type": "function",
}
Expand Down
10 changes: 5 additions & 5 deletions sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
from typing import Any, Union

try:
from pydantic_ai.messages import BinaryContent, ImageUrl # type: ignore
from pydantic_ai.messages import BinaryContent, ImageUrl
except ImportError:
BinaryContent = None
ImageUrl = None
BinaryContent = None # type: ignore[misc,assignment]
ImageUrl = None # type: ignore[misc,assignment]


def invoke_agent_span(
Expand Down Expand Up @@ -123,9 +123,9 @@ def invoke_agent_span(
for item in user_prompt:
if isinstance(item, str):
content.append({"text": item, "type": "text"})
elif ImageUrl and isinstance(item, ImageUrl):
elif ImageUrl is not None and isinstance(item, ImageUrl):
content.append(_serialize_image_url_item(item))
elif BinaryContent and isinstance(item, BinaryContent):
elif BinaryContent is not None and isinstance(item, BinaryContent):
content.append(_serialize_binary_content_item(item))
if content:
messages.append(
Expand Down
2 changes: 1 addition & 1 deletion sentry_sdk/integrations/pydantic_ai/spans/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
if TYPE_CHECKING:
from typing import Any, Dict, Union

from pydantic_ai.usage import RequestUsage, RunUsage # type: ignore
from pydantic_ai.usage import RequestUsage, RunUsage


def _serialize_image_url_item(item: "Any") -> "Dict[str, Any]":
Expand Down
Loading
Loading