diff --git a/sentry_sdk/integrations/pydantic_ai/__init__.py b/sentry_sdk/integrations/pydantic_ai/__init__.py index 0f0de53fa5..648f6503ec 100644 --- a/sentry_sdk/integrations/pydantic_ai/__init__.py +++ b/sentry_sdk/integrations/pydantic_ai/__init__.py @@ -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") @@ -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 @@ -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) diff --git a/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py b/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py index eaa4385834..15f2b1994c 100644 --- a/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +++ b/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py @@ -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": @@ -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 metadata is None: + kwargs["metadata"] = {"_sentry_span": None} + # Create invoke_agent span with invoke_agent_span( user_prompt, self, model, model_settings, is_streaming @@ -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": @@ -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 metadata is None: + kwargs["metadata"] = {"_sentry_span": None} + # Call original function to get the context manager original_ctx_manager = original_func(self, *args, **kwargs) diff --git a/tests/integrations/pydantic_ai/test_pydantic_ai.py b/tests/integrations/pydantic_ai/test_pydantic_ai.py index dfbd1c2e94..50ce155f5b 100644 --- a/tests/integrations/pydantic_ai/test_pydantic_ai.py +++ b/tests/integrations/pydantic_ai/test_pydantic_ai.py @@ -16,37 +16,44 @@ from pydantic_ai.messages import BinaryContent, ImageUrl, UserPromptPart from pydantic_ai.usage import RequestUsage from pydantic_ai.exceptions import ModelRetry, UnexpectedModelBehavior +from pydantic_ai.models.function import FunctionModel @pytest.fixture -def test_agent(): - """Create a test agent with model settings.""" - return Agent( - "test", - name="test_agent", - system_prompt="You are a helpful test assistant.", - ) +def get_test_agent(): + def inner(): + """Create a test agent with model settings.""" + return Agent( + "test", + name="test_agent", + system_prompt="You are a helpful test assistant.", + ) + + return inner @pytest.fixture -def test_agent_with_settings(): - """Create a test agent with explicit model settings.""" - from pydantic_ai import ModelSettings +def get_test_agent_with_settings(): + def inner(): + """Create a test agent with explicit model settings.""" + from pydantic_ai import ModelSettings + + return Agent( + "test", + name="test_agent_settings", + system_prompt="You are a test assistant with settings.", + model_settings=ModelSettings( + temperature=0.7, + max_tokens=100, + top_p=0.9, + ), + ) - return Agent( - "test", - name="test_agent_settings", - system_prompt="You are a test assistant with settings.", - model_settings=ModelSettings( - temperature=0.7, - max_tokens=100, - top_p=0.9, - ), - ) + return inner @pytest.mark.asyncio -async def test_agent_run_async(sentry_init, capture_events, test_agent): +async def test_agent_run_async(sentry_init, capture_events, get_test_agent): """ Test that the integration creates spans for async agent runs. """ @@ -58,6 +65,7 @@ async def test_agent_run_async(sentry_init, capture_events, test_agent): events = capture_events() + test_agent = get_test_agent() result = await test_agent.run("Test input") assert result is not None @@ -88,7 +96,36 @@ async def test_agent_run_async(sentry_init, capture_events, test_agent): @pytest.mark.asyncio -async def test_agent_run_async_usage_data(sentry_init, capture_events, test_agent): +async def test_agent_run_async_model_error(sentry_init, capture_events): + sentry_init( + integrations=[PydanticAIIntegration()], + traces_sample_rate=1.0, + ) + + events = capture_events() + + def failing_model(messages, info): + raise RuntimeError("model exploded") + + agent = Agent( + FunctionModel(failing_model), + name="test_agent", + ) + + with pytest.raises(RuntimeError, match="model exploded"): + await agent.run("Test input") + + (error, transaction) = events + assert error["level"] == "error" + + spans = transaction["spans"] + assert len(spans) == 1 + + assert spans[0]["status"] == "internal_error" + + +@pytest.mark.asyncio +async def test_agent_run_async_usage_data(sentry_init, capture_events, get_test_agent): """ Test that the invoke_agent span includes token usage and model data. """ @@ -100,6 +137,7 @@ async def test_agent_run_async_usage_data(sentry_init, capture_events, test_agen events = capture_events() + test_agent = get_test_agent() result = await test_agent.run("Test input") assert result is not None @@ -132,7 +170,7 @@ async def test_agent_run_async_usage_data(sentry_init, capture_events, test_agen assert trace_data["gen_ai.response.model"] == "test" # Test model name -def test_agent_run_sync(sentry_init, capture_events, test_agent): +def test_agent_run_sync(sentry_init, capture_events, get_test_agent): """ Test that the integration creates spans for sync agent runs. """ @@ -144,6 +182,7 @@ def test_agent_run_sync(sentry_init, capture_events, test_agent): events = capture_events() + test_agent = get_test_agent() result = test_agent.run_sync("Test input") assert result is not None @@ -165,8 +204,36 @@ def test_agent_run_sync(sentry_init, capture_events, test_agent): assert chat_span["data"]["gen_ai.response.streaming"] is False +def test_agent_run_sync_model_error(sentry_init, capture_events): + sentry_init( + integrations=[PydanticAIIntegration()], + traces_sample_rate=1.0, + ) + + events = capture_events() + + def failing_model(messages, info): + raise RuntimeError("model exploded") + + agent = Agent( + FunctionModel(failing_model), + name="test_agent", + ) + + with pytest.raises(RuntimeError, match="model exploded"): + agent.run_sync("Test input") + + (error, transaction) = events + assert error["level"] == "error" + + spans = transaction["spans"] + assert len(spans) == 1 + + assert spans[0]["status"] == "internal_error" + + @pytest.mark.asyncio -async def test_agent_run_stream(sentry_init, capture_events, test_agent): +async def test_agent_run_stream(sentry_init, capture_events, get_test_agent): """ Test that the integration creates spans for streaming agent runs. """ @@ -178,6 +245,7 @@ async def test_agent_run_stream(sentry_init, capture_events, test_agent): events = capture_events() + test_agent = get_test_agent() async with test_agent.run_stream("Test input") as result: # Consume the stream async for _ in result.stream_output(): @@ -207,7 +275,7 @@ async def test_agent_run_stream(sentry_init, capture_events, test_agent): @pytest.mark.asyncio -async def test_agent_run_stream_events(sentry_init, capture_events, test_agent): +async def test_agent_run_stream_events(sentry_init, capture_events, get_test_agent): """ Test that run_stream_events creates spans (it uses run internally, so non-streaming). """ @@ -220,6 +288,7 @@ async def test_agent_run_stream_events(sentry_init, capture_events, test_agent): events = capture_events() # Consume all events + test_agent = get_test_agent() async for _ in test_agent.run_stream_events("Test input"): pass @@ -239,22 +308,23 @@ async def test_agent_run_stream_events(sentry_init, capture_events, test_agent): @pytest.mark.asyncio -async def test_agent_with_tools(sentry_init, capture_events, test_agent): +async def test_agent_with_tools(sentry_init, capture_events, get_test_agent): """ Test that tool execution creates execute_tool spans. """ - - @test_agent.tool_plain - def add_numbers(a: int, b: int) -> int: - """Add two numbers together.""" - return a + b - sentry_init( integrations=[PydanticAIIntegration()], traces_sample_rate=1.0, send_default_pii=True, ) + test_agent = get_test_agent() + + @test_agent.tool_plain + def add_numbers(a: int, b: int) -> int: + """Add two numbers together.""" + return a + b + events = capture_events() result = await test_agent.run("What is 5 + 3?") @@ -293,14 +363,25 @@ def add_numbers(a: int, b: int) -> int: ) @pytest.mark.asyncio async def test_agent_with_tool_model_retry( - sentry_init, capture_events, test_agent, handled_tool_call_exceptions + sentry_init, capture_events, get_test_agent, handled_tool_call_exceptions ): """ Test that a handled exception is captured when a tool raises ModelRetry. """ + sentry_init( + integrations=[ + PydanticAIIntegration( + handled_tool_call_exceptions=handled_tool_call_exceptions + ) + ], + traces_sample_rate=1.0, + send_default_pii=True, + ) retries = 0 + test_agent = get_test_agent() + @test_agent.tool_plain def add_numbers(a: int, b: int) -> float: """Add two numbers together, but raises an exception on the first attempt.""" @@ -310,16 +391,6 @@ def add_numbers(a: int, b: int) -> float: raise ModelRetry(message="Try again with the same arguments.") return a + b - sentry_init( - integrations=[ - PydanticAIIntegration( - handled_tool_call_exceptions=handled_tool_call_exceptions - ) - ], - traces_sample_rate=1.0, - send_default_pii=True, - ) - events = capture_events() result = await test_agent.run("What is 5 + 3?") @@ -371,17 +442,11 @@ def add_numbers(a: int, b: int) -> float: ) @pytest.mark.asyncio async def test_agent_with_tool_validation_error( - sentry_init, capture_events, test_agent, handled_tool_call_exceptions + sentry_init, capture_events, get_test_agent, handled_tool_call_exceptions ): """ Test that a handled exception is captured when a tool has unsatisfiable constraints. """ - - @test_agent.tool_plain - def add_numbers(a: Annotated[int, Field(gt=0, lt=0)], b: int) -> int: - """Add two numbers together.""" - return a + b - sentry_init( integrations=[ PydanticAIIntegration( @@ -392,6 +457,13 @@ def add_numbers(a: Annotated[int, Field(gt=0, lt=0)], b: int) -> int: send_default_pii=True, ) + test_agent = get_test_agent() + + @test_agent.tool_plain + def add_numbers(a: Annotated[int, Field(gt=0, lt=0)], b: int) -> int: + """Add two numbers together.""" + return a + b + events = capture_events() result = None @@ -436,22 +508,23 @@ def add_numbers(a: Annotated[int, Field(gt=0, lt=0)], b: int) -> int: @pytest.mark.asyncio -async def test_agent_with_tools_streaming(sentry_init, capture_events, test_agent): +async def test_agent_with_tools_streaming(sentry_init, capture_events, get_test_agent): """ Test that tool execution works correctly with streaming. """ - - @test_agent.tool_plain - def multiply(a: int, b: int) -> int: - """Multiply two numbers.""" - return a * b - sentry_init( integrations=[PydanticAIIntegration()], traces_sample_rate=1.0, send_default_pii=True, ) + test_agent = get_test_agent() + + @test_agent.tool_plain + def multiply(a: int, b: int) -> int: + """Multiply two numbers.""" + return a * b + events = capture_events() async with test_agent.run_stream("What is 7 times 8?") as result: @@ -480,7 +553,9 @@ def multiply(a: int, b: int) -> int: @pytest.mark.asyncio -async def test_model_settings(sentry_init, capture_events, test_agent_with_settings): +async def test_model_settings( + sentry_init, capture_events, get_test_agent_with_settings +): """ Test that model settings are captured in spans. """ @@ -491,6 +566,7 @@ async def test_model_settings(sentry_init, capture_events, test_agent_with_setti events = capture_events() + test_agent_with_settings = get_test_agent_with_settings() await test_agent_with_settings.run("Test input") (transaction,) = events @@ -592,7 +668,7 @@ async def test_error_handling(sentry_init, capture_events): @pytest.mark.asyncio -async def test_without_pii(sentry_init, capture_events, test_agent): +async def test_without_pii(sentry_init, capture_events, get_test_agent): """ Test that PII is not captured when send_default_pii is False. """ @@ -604,6 +680,7 @@ async def test_without_pii(sentry_init, capture_events, test_agent): events = capture_events() + test_agent = get_test_agent() await test_agent.run("Sensitive input") (transaction,) = events @@ -619,22 +696,23 @@ async def test_without_pii(sentry_init, capture_events, test_agent): @pytest.mark.asyncio -async def test_without_pii_tools(sentry_init, capture_events, test_agent): +async def test_without_pii_tools(sentry_init, capture_events, get_test_agent): """ Test that tool input/output are not captured when send_default_pii is False. """ - - @test_agent.tool_plain - def sensitive_tool(data: str) -> str: - """A tool with sensitive data.""" - return f"Processed: {data}" - sentry_init( integrations=[PydanticAIIntegration()], traces_sample_rate=1.0, send_default_pii=False, ) + test_agent = get_test_agent() + + @test_agent.tool_plain + def sensitive_tool(data: str) -> str: + """A tool with sensitive data.""" + return f"Processed: {data}" + events = capture_events() await test_agent.run("Use sensitive tool with private data") @@ -652,7 +730,7 @@ def sensitive_tool(data: str) -> str: @pytest.mark.asyncio -async def test_multiple_agents_concurrent(sentry_init, capture_events, test_agent): +async def test_multiple_agents_concurrent(sentry_init, capture_events, get_test_agent): """ Test that multiple agents can run concurrently without interfering. """ @@ -663,6 +741,8 @@ async def test_multiple_agents_concurrent(sentry_init, capture_events, test_agen events = capture_events() + test_agent = get_test_agent() + async def run_agent(input_text): return await test_agent.run(input_text) @@ -733,7 +813,7 @@ async def test_message_history(sentry_init, capture_events): @pytest.mark.asyncio -async def test_gen_ai_system(sentry_init, capture_events, test_agent): +async def test_gen_ai_system(sentry_init, capture_events, get_test_agent): """ Test that gen_ai.system is set from the model. """ @@ -744,6 +824,7 @@ async def test_gen_ai_system(sentry_init, capture_events, test_agent): events = capture_events() + test_agent = get_test_agent() await test_agent.run("Test input") (transaction,) = events @@ -760,7 +841,7 @@ async def test_gen_ai_system(sentry_init, capture_events, test_agent): @pytest.mark.asyncio -async def test_include_prompts_false(sentry_init, capture_events, test_agent): +async def test_include_prompts_false(sentry_init, capture_events, get_test_agent): """ Test that prompts are not captured when include_prompts=False. """ @@ -772,6 +853,7 @@ async def test_include_prompts_false(sentry_init, capture_events, test_agent): events = capture_events() + test_agent = get_test_agent() await test_agent.run("Sensitive prompt") (transaction,) = events @@ -787,7 +869,7 @@ async def test_include_prompts_false(sentry_init, capture_events, test_agent): @pytest.mark.asyncio -async def test_include_prompts_true(sentry_init, capture_events, test_agent): +async def test_include_prompts_true(sentry_init, capture_events, get_test_agent): """ Test that prompts are captured when include_prompts=True (default). """ @@ -799,6 +881,7 @@ async def test_include_prompts_true(sentry_init, capture_events, test_agent): events = capture_events() + test_agent = get_test_agent() await test_agent.run("Test prompt") (transaction,) = events @@ -815,23 +898,24 @@ async def test_include_prompts_true(sentry_init, capture_events, test_agent): @pytest.mark.asyncio async def test_include_prompts_false_with_tools( - sentry_init, capture_events, test_agent + sentry_init, capture_events, get_test_agent ): """ Test that tool input/output are not captured when include_prompts=False. """ - - @test_agent.tool_plain - def test_tool(value: int) -> int: - """A test tool.""" - return value * 2 - sentry_init( integrations=[PydanticAIIntegration(include_prompts=False)], traces_sample_rate=1.0, send_default_pii=True, ) + test_agent = get_test_agent() + + @test_agent.tool_plain + def test_tool(value: int) -> int: + """A test tool.""" + return value * 2 + events = capture_events() await test_agent.run("Use the test tool with value 5") @@ -849,7 +933,9 @@ def test_tool(value: int) -> int: @pytest.mark.asyncio -async def test_include_prompts_requires_pii(sentry_init, capture_events, test_agent): +async def test_include_prompts_requires_pii( + sentry_init, capture_events, get_test_agent +): """ Test that include_prompts requires send_default_pii=True. """ @@ -861,6 +947,7 @@ async def test_include_prompts_requires_pii(sentry_init, capture_events, test_ag events = capture_events() + test_agent = get_test_agent() await test_agent.run("Test prompt") (transaction,) = events @@ -1011,7 +1098,7 @@ async def mock_map_tool_result_part(part): @pytest.mark.asyncio -async def test_context_cleanup_after_run(sentry_init, test_agent): +async def test_context_cleanup_after_run(sentry_init, get_test_agent): """ Test that the pydantic_ai_agent context is properly cleaned up after agent execution. """ @@ -1027,13 +1114,14 @@ async def test_context_cleanup_after_run(sentry_init, test_agent): assert "pydantic_ai_agent" not in scope._contexts # Run the agent + test_agent = get_test_agent() await test_agent.run("Test input") # Verify context is cleaned up after run assert "pydantic_ai_agent" not in scope._contexts -def test_context_cleanup_after_run_sync(sentry_init, test_agent): +def test_context_cleanup_after_run_sync(sentry_init, get_test_agent): """ Test that the pydantic_ai_agent context is properly cleaned up after sync agent execution. """ @@ -1049,6 +1137,7 @@ def test_context_cleanup_after_run_sync(sentry_init, test_agent): assert "pydantic_ai_agent" not in scope._contexts # Run the agent synchronously + test_agent = get_test_agent() test_agent.run_sync("Test input") # Verify context is cleaned up after run @@ -1056,7 +1145,7 @@ def test_context_cleanup_after_run_sync(sentry_init, test_agent): @pytest.mark.asyncio -async def test_context_cleanup_after_streaming(sentry_init, test_agent): +async def test_context_cleanup_after_streaming(sentry_init, get_test_agent): """ Test that the pydantic_ai_agent context is properly cleaned up after streaming execution. """ @@ -1071,6 +1160,7 @@ async def test_context_cleanup_after_streaming(sentry_init, test_agent): scope = sentry_sdk.get_current_scope() assert "pydantic_ai_agent" not in scope._contexts + test_agent = get_test_agent() # Run the agent with streaming async with test_agent.run_stream("Test input") as result: async for _ in result.stream_output(): @@ -1081,23 +1171,25 @@ async def test_context_cleanup_after_streaming(sentry_init, test_agent): @pytest.mark.asyncio -async def test_context_cleanup_on_error(sentry_init, test_agent): +async def test_context_cleanup_on_error(sentry_init, get_test_agent): """ Test that the pydantic_ai_agent context is cleaned up even when an error occurs. """ import sentry_sdk + sentry_init( + integrations=[PydanticAIIntegration()], + traces_sample_rate=1.0, + ) + + test_agent = get_test_agent() + # Create an agent with a tool that raises an error @test_agent.tool_plain def failing_tool() -> str: """A tool that always fails.""" raise ValueError("Tool error") - sentry_init( - integrations=[PydanticAIIntegration()], - traces_sample_rate=1.0, - ) - # Verify context is not set before run scope = sentry_sdk.get_current_scope() assert "pydantic_ai_agent" not in scope._contexts @@ -1113,7 +1205,7 @@ def failing_tool() -> str: @pytest.mark.asyncio -async def test_context_isolation_concurrent_agents(sentry_init, test_agent): +async def test_context_isolation_concurrent_agents(sentry_init, get_test_agent): """ Test that concurrent agent executions maintain isolated contexts. """ @@ -1146,6 +1238,7 @@ async def run_and_check_context(agent, agent_name): return agent_name + test_agent = get_test_agent() # Run both agents concurrently results = await asyncio.gather( run_and_check_context(test_agent, "agent1"), @@ -1399,22 +1492,23 @@ async def test_agent_data_from_scope(sentry_init, capture_events): @pytest.mark.asyncio async def test_available_tools_without_description( - sentry_init, capture_events, test_agent + sentry_init, capture_events, get_test_agent ): """ Test that available tools are captured even when description is missing. """ + sentry_init( + integrations=[PydanticAIIntegration()], + traces_sample_rate=1.0, + ) + + test_agent = get_test_agent() @test_agent.tool_plain def tool_without_desc(x: int) -> int: # No docstring = no description return x * 2 - sentry_init( - integrations=[PydanticAIIntegration()], - traces_sample_rate=1.0, - ) - events = capture_events() await test_agent.run("Use the tool with 5") @@ -1431,22 +1525,23 @@ def tool_without_desc(x: int) -> int: @pytest.mark.asyncio -async def test_output_with_tool_calls(sentry_init, capture_events, test_agent): +async def test_output_with_tool_calls(sentry_init, capture_events, get_test_agent): """ Test that tool calls in model response are captured correctly. """ - - @test_agent.tool_plain - def calc_tool(value: int) -> int: - """Calculate something.""" - return value + 10 - sentry_init( integrations=[PydanticAIIntegration()], traces_sample_rate=1.0, send_default_pii=True, ) + test_agent = get_test_agent() + + @test_agent.tool_plain + def calc_tool(value: int) -> int: + """Calculate something.""" + return value + 10 + events = capture_events() await test_agent.run("Use calc_tool with 5") @@ -1633,7 +1728,6 @@ async def test_input_messages_error_handling(sentry_init, capture_events): Test that _set_input_messages handles errors gracefully. """ import sentry_sdk - from sentry_sdk.integrations.pydantic_ai.spans.ai_client import _set_input_messages sentry_init( integrations=[PydanticAIIntegration()], @@ -1787,7 +1881,6 @@ async def test_message_parts_with_list_content(sentry_init, capture_events): """ import sentry_sdk from unittest.mock import MagicMock - from sentry_sdk.integrations.pydantic_ai.spans.ai_client import _set_input_messages sentry_init( integrations=[PydanticAIIntegration()], @@ -1894,7 +1987,6 @@ async def test_message_with_system_prompt_part(sentry_init, capture_events): """ import sentry_sdk from unittest.mock import MagicMock - from sentry_sdk.integrations.pydantic_ai.spans.ai_client import _set_input_messages from pydantic_ai import messages sentry_init( @@ -1931,7 +2023,6 @@ async def test_message_with_instructions(sentry_init, capture_events): """ import sentry_sdk from unittest.mock import MagicMock - from sentry_sdk.integrations.pydantic_ai.spans.ai_client import _set_input_messages sentry_init( integrations=[PydanticAIIntegration()], @@ -1966,7 +2057,6 @@ async def test_set_input_messages_without_prompts(sentry_init, capture_events): Test that _set_input_messages respects _should_send_prompts(). """ import sentry_sdk - from sentry_sdk.integrations.pydantic_ai.spans.ai_client import _set_input_messages sentry_init( integrations=[PydanticAIIntegration(include_prompts=False)],