diff --git a/pyproject.toml b/pyproject.toml index 356fa4227c..5ac71fb998 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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'", diff --git a/sentry_sdk/integrations/pydantic_ai/__init__.py b/sentry_sdk/integrations/pydantic_ai/__init__.py index 81e7cf8090..db21861e71 100644 --- a/sentry_sdk/integrations/pydantic_ai/__init__.py +++ b/sentry_sdk/integrations/pydantic_ai/__init__.py @@ -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") @@ -24,8 +24,8 @@ 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: @@ -33,7 +33,7 @@ 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": @@ -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]", *, @@ -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]", *, @@ -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): diff --git a/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py b/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py index 3039e40698..864c83a506 100644 --- a/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +++ b/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py @@ -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") @@ -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] diff --git a/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py b/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py index afb10395f4..c2a033537c 100644 --- a/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +++ b/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py @@ -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") @@ -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 @@ -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] diff --git a/sentry_sdk/integrations/pydantic_ai/patches/tools.py b/sentry_sdk/integrations/pydantic_ai/patches/tools.py index 5646b5d47c..958d729fbb 100644 --- a/sentry_sdk/integrations/pydantic_ai/patches/tools.py +++ b/sentry_sdk/integrations/pydantic_ai/patches/tools.py @@ -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") @@ -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: @@ -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( @@ -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] diff --git a/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py b/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py index 27deb0c55c..263e280e11 100644 --- a/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +++ b/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py @@ -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 @@ -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( @@ -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: @@ -141,15 +141,22 @@ 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]" = [] @@ -157,7 +164,9 @@ def _set_input_messages( 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 @@ -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"): @@ -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)) @@ -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", } diff --git a/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py b/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py index f0c68e85ba..876cb1ecea 100644 --- a/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +++ b/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py @@ -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( @@ -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( diff --git a/sentry_sdk/integrations/pydantic_ai/spans/utils.py b/sentry_sdk/integrations/pydantic_ai/spans/utils.py index 330496c6b2..cbce0c0c77 100644 --- a/sentry_sdk/integrations/pydantic_ai/spans/utils.py +++ b/sentry_sdk/integrations/pydantic_ai/spans/utils.py @@ -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]": diff --git a/uv.lock b/uv.lock index fb1818bba8..b7c157b728 100644 --- a/uv.lock +++ b/uv.lock @@ -52,6 +52,7 @@ typing = [ { name = "openfeature-sdk" }, { name = "opentelemetry-distro", extras = ["otlp"] }, { name = "pydantic", specifier = ">=2.13.4" }, + { name = "pydantic-ai-slim", specifier = ">=2.23.0" }, { name = "pymongo" }, { name = "python-multipart", specifier = ">=0.0.32" }, { name = "starlette", specifier = ">=1.3.1" }, @@ -519,6 +520,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] +[[package]] +name = "genai-prices" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx2" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/9b/85e646305a90a2da18f1edf055498668391e71f9849d3e1754d66559a311/genai_prices-0.1.1.tar.gz", hash = "sha256:54a2237691e0aaefb057d10a0c3c20160accc9fc09521c64c03fcdb7a4a69f68", size = 91182, upload-time = "2026-08-01T09:02:49.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/5e/cfe36dff790ffad6aeff8a069b6f36743987ac17053579035ee0a67635dd/genai_prices-0.1.1-py3-none-any.whl", hash = "sha256:de2e3d8ea3ca1d0d292025995c598da447a74e94f22cd3342df46941aeb5416b", size = 95300, upload-time = "2026-08-01T09:02:48.308Z" }, +] + [[package]] name = "gevent" version = "26.5.0" @@ -619,6 +633,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590, upload-time = "2026-05-20T13:13:37.382Z" }, ] +[[package]] +name = "griffelib" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, +] + [[package]] name = "grpcio" version = "1.80.0" @@ -942,6 +965,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, ] +[[package]] +name = "logfire-api" +version = "4.39.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/f4/41f8647f6091fb9b9aac5a4b6d164bddb11d55b8369bf38de154ef91b8f4/logfire_api-4.39.0.tar.gz", hash = "sha256:1e885f95c37d58cdb927bbc6baea4f4a7c13066f6b3019758627d4dc442643d0", size = 90619, upload-time = "2026-07-24T18:31:36.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/d4/87747d12eaf2d852676fd6535df76df945ef62681f1eb5391b63d1fc05e2/logfire_api-4.39.0-py3-none-any.whl", hash = "sha256:20057bbd2898dec2eed02e2559bd73f4e10bc4b108987821df55e9c762da3ba8", size = 140413, upload-time = "2026-07-24T18:31:32.818Z" }, +] + [[package]] name = "loguru" version = "0.7.3" @@ -1394,6 +1426,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] +[[package]] +name = "pydantic-ai-slim" +version = "2.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "genai-prices" }, + { name = "griffelib" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pydantic-graph" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/9f/53b19efefa041c1080f7c4ad41679a9293cce64f1265168a98cbe06a0ab7/pydantic_ai_slim-2.23.0.tar.gz", hash = "sha256:d16dcbfb2bfea0ee162bf0f499442fab5a4d69b41e4c54f3b694c2e90b983768", size = 965485, upload-time = "2026-08-04T01:58:20.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6f/539a255524178a8421d582271a8d7f8667b036f02b4ddc4f20abcc63888b/pydantic_ai_slim-2.23.0-py3-none-any.whl", hash = "sha256:a2fa3e56408bbf1b83900e3dd4ad9b137297742f450863c2f0f9a03a547d0e33", size = 1157486, upload-time = "2026-08-04T01:58:12.356Z" }, +] + [[package]] name = "pydantic-core" version = "2.46.4" @@ -1435,6 +1486,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, ] +[[package]] +name = "pydantic-graph" +version = "2.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/fc/273bac7d14fb62c060e0c20c51a9cc60e9e90a96d992fd20e77abf4b6ac1/pydantic_graph-2.23.0.tar.gz", hash = "sha256:54c9939f47fd8a268c96320d7d90e7cef037cbfd2625a675dc4028c1377f70ab", size = 45179, upload-time = "2026-08-04T01:58:23.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/c4/875cf853d205dc55422bd44ff0fbfac82e6e34ff7693df016fc3a4088d32/pydantic_graph-2.23.0-py3-none-any.whl", hash = "sha256:b0f12b4f72adb2a5522b5962c95e1a7b140cb3f631a628036f935e291a9e50ba", size = 52662, upload-time = "2026-08-04T01:58:15.858Z" }, +] + [[package]] name = "pydantic-settings" version = "2.14.2"