diff --git a/sentry_sdk/integrations/openai_agents/__init__.py b/sentry_sdk/integrations/openai_agents/__init__.py index 5895f53ad3..f42751bf1b 100644 --- a/sentry_sdk/integrations/openai_agents/__init__.py +++ b/sentry_sdk/integrations/openai_agents/__init__.py @@ -46,17 +46,19 @@ from agents.run_internal.run_steps import SingleStepResult -def _patch_runner() -> None: +def _patch_runner(use_tool_hooks: "bool") -> None: # Create the root span for one full agent run (including eventual handoffs) # Note agents.run.DEFAULT_AGENT_RUNNER.run_sync is a wrapper around # agents.run.DEFAULT_AGENT_RUNNER.run. It does not need to be wrapped separately. agents.run.DEFAULT_AGENT_RUNNER.run = _create_run_wrapper( - agents.run.DEFAULT_AGENT_RUNNER.run + agents.run.DEFAULT_AGENT_RUNNER.run, + use_tool_hooks=use_tool_hooks, ) # Patch streaming runner agents.run.DEFAULT_AGENT_RUNNER.run_streamed = _create_run_streamed_wrapper( - agents.run.DEFAULT_AGENT_RUNNER.run_streamed + agents.run.DEFAULT_AGENT_RUNNER.run_streamed, + use_tool_hooks=use_tool_hooks, ) @@ -92,26 +94,19 @@ class OpenAIAgentsIntegration(Integration): @staticmethod def setup_once() -> None: _patch_error_tracing() - _patch_runner() library_version = parse_version(OPENAI_AGENTS_VERSION) + # ToolContext.tool_arguments added in https://github.com/openai/openai-agents-python/commit/5e1db14da542c77f8fdd5e2e26017977ae415813 + use_tool_hooks = library_version is not None and library_version >= (0, 3, 2) + + _patch_runner(use_tool_hooks=use_tool_hooks) + if library_version is not None and library_version >= ( 0, 8, ): if run_loop is not None: - @wraps(run_loop.get_all_tools) - async def new_wrapped_get_all_tools( - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", - ) -> "list[agents.Tool]": - return await _get_all_tools( - run_loop.get_all_tools, agent, context_wrapper - ) - - agents.run.get_all_tools = new_wrapped_get_all_tools - @wraps(run_loop.run_single_turn) async def new_wrapped_run_single_turn( *args: "Any", **kwargs: "Any" @@ -175,17 +170,22 @@ async def new_wrapped_final_output( return - original_get_all_tools = AgentRunner._get_all_tools - - @wraps(AgentRunner._get_all_tools.__func__) - async def old_wrapped_get_all_tools( - cls: "agents.Runner", - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", - ) -> "list[agents.Tool]": - return await _get_all_tools(original_get_all_tools, agent, context_wrapper) + if not use_tool_hooks: + original_get_all_tools = AgentRunner._get_all_tools + + @wraps(AgentRunner._get_all_tools.__func__) + async def old_wrapped_get_all_tools( + cls: "agents.Runner", + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", + ) -> "list[agents.Tool]": + return await _get_all_tools( + original_get_all_tools, agent, context_wrapper + ) - agents.run.AgentRunner._get_all_tools = classmethod(old_wrapped_get_all_tools) + agents.run.AgentRunner._get_all_tools = classmethod( + old_wrapped_get_all_tools + ) original_get_model = AgentRunner._get_model diff --git a/sentry_sdk/integrations/openai_agents/patches/runner.py b/sentry_sdk/integrations/openai_agents/patches/runner.py index 5f9996595f..78a4bcbb26 100644 --- a/sentry_sdk/integrations/openai_agents/patches/runner.py +++ b/sentry_sdk/integrations/openai_agents/patches/runner.py @@ -4,24 +4,111 @@ import sentry_sdk from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.scope import should_send_default_pii from sentry_sdk.traces import StreamedSpan from sentry_sdk.utils import capture_internal_exceptions, reraise -from ..spans import agent_workflow_span, update_invoke_agent_span +from ..spans import ( + agent_workflow_span, + execute_tool_span, + update_execute_tool_span, + update_invoke_agent_span, +) from ..utils import _capture_exception try: + from agents import FunctionTool, RunHooks from agents.exceptions import AgentsException except ImportError: raise DidNotEnable("OpenAI Agents not installed") -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypeVar if TYPE_CHECKING: from typing import Any, AsyncIterator, Callable + from agents import Agent, Tool, ToolContext -def _create_run_wrapper(original_func: "Callable[..., Any]") -> "Callable[..., Any]": + +TContext = TypeVar("TContext") + + +class _SentryRunHooks(RunHooks[TContext]): # type: ignore[misc] + async def on_tool_start( + self, + context: "ToolContext[TContext]", + agent: "Agent[TContext]", + tool: "Tool", + ) -> "None": + if not isinstance(tool, FunctionTool): + return + + span = execute_tool_span(tool, agent) + span.__enter__() + context.sentry_tool_span = span + + if not should_send_default_pii(): + return + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_TOOL_INPUT, context.tool_arguments) + else: + span.set_data(SPANDATA.GEN_AI_TOOL_INPUT, context.tool_arguments) + + async def on_tool_end( + self, + context: "ToolContext[TContext]", + agent: "Agent[TContext]", + tool: "Tool", + result: "object", + ) -> "None": + if not isinstance(tool, FunctionTool): + return + + span = getattr(context, "sentry_tool_span", None) + if span is not None: + del context.sentry_tool_span + update_execute_tool_span(span, agent, tool, result) + span.__exit__(None, None, None) + + +def _patch_run_hooks(hooks: "RunHooks[TContext]") -> None: + is_already_patched = getattr(hooks, "_sentry_is_patched", False) + if is_already_patched: + return + + original_on_tool_start = hooks.on_tool_start + original_on_tool_end = hooks.on_tool_end + + sentry_hooks = _SentryRunHooks() # type: ignore[var-annotated] + + @wraps(original_on_tool_start) + async def on_tool_start( + context: "ToolContext[TContext]", agent: "Agent[TContext]", tool: "Tool" + ) -> "None": + with capture_internal_exceptions(): + await sentry_hooks.on_tool_start(context, agent, tool) + await original_on_tool_start(context, agent, tool) + + @wraps(original_on_tool_end) + async def on_tool_end( + context: "ToolContext[TContext]", + agent: "Agent[TContext]", + tool: "Tool", + result: "object", + ) -> "None": + with capture_internal_exceptions(): + await sentry_hooks.on_tool_end(context, agent, tool, result) + await original_on_tool_end(context, agent, tool, result) + + hooks._sentry_is_patched = True + hooks.on_tool_start = on_tool_start + hooks.on_tool_end = on_tool_end + + +def _create_run_wrapper( + original_func: "Callable[..., Any]", use_tool_hooks: "bool" +) -> "Callable[..., Any]": """ Wraps the agents.Runner.run methods to - create and manage a root span for the agent workflow runs. @@ -33,6 +120,13 @@ def _create_run_wrapper(original_func: "Callable[..., Any]") -> "Callable[..., A @wraps(original_func) async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + if use_tool_hooks: + hooks = kwargs.get("hooks") + if hooks is not None: + _patch_run_hooks(hooks=hooks) + else: + kwargs["hooks"] = _SentryRunHooks() + # Isolate each workflow so that when agents are run in asyncio tasks they # don't touch each other's scopes with sentry_sdk.isolation_scope(): @@ -123,7 +217,7 @@ async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": def _create_run_streamed_wrapper( - original_func: "Callable[..., Any]", + original_func: "Callable[..., Any]", use_tool_hooks: "bool" ) -> "Callable[..., Any]": """ Wraps the agents.Runner.run_streamed method to @@ -173,6 +267,14 @@ def wrapper(*args: "Any", **kwargs: "Any") -> "Any": else: args = (agent, *args[1:]) + if use_tool_hooks: + sentry_hooks = _SentryRunHooks() # type: ignore[var-annotated] + hooks = kwargs.get("hooks") + if hooks is not None: + _patch_run_hooks(hooks=hooks) + else: + kwargs["hooks"] = sentry_hooks + try: # Call original function to get RunResultStreaming run_result = original_func(*args, **kwargs) diff --git a/sentry_sdk/integrations/openai_agents/patches/tools.py b/sentry_sdk/integrations/openai_agents/patches/tools.py index ab49df8b9e..2cb0a972f8 100644 --- a/sentry_sdk/integrations/openai_agents/patches/tools.py +++ b/sentry_sdk/integrations/openai_agents/patches/tools.py @@ -1,7 +1,10 @@ from functools import wraps from typing import TYPE_CHECKING +from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan from ..spans import execute_tool_span, update_execute_tool_span @@ -53,6 +56,14 @@ async def sentry_wrapped_on_invoke_tool( result = await current_on_invoke(*args, **kwargs) update_execute_tool_span(span, agent, current_tool, result) + if not should_send_default_pii(): + return result + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_TOOL_INPUT, args[1]) + else: + span.set_data(SPANDATA.GEN_AI_TOOL_INPUT, args[1]) + return result return sentry_wrapped_on_invoke_tool diff --git a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/sentry_sdk/integrations/openai_agents/spans/execute_tool.py index fd3a430951..7e1861757d 100644 --- a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py +++ b/sentry_sdk/integrations/openai_agents/spans/execute_tool.py @@ -30,8 +30,6 @@ def execute_tool_span( SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description, }, ) - - set_on_span = span.set_attribute else: span = sentry_sdk.start_span( op=OP.GEN_AI_EXECUTE_TOOL, @@ -44,12 +42,6 @@ def execute_tool_span( span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name) span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description) - set_on_span = span.set_data - - if should_send_default_pii(): - input = args[1] - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input) - return span diff --git a/tests/conftest.py b/tests/conftest.py index 6b406d6a06..f3ae302057 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1500,7 +1500,7 @@ def nonstreaming_google_genai_model_response(): @pytest.fixture -def responses_tool_call_model_responses(): +def nonstreaming_responses_tool_call_model_responses(): def inner( tool_name: str, arguments: str, @@ -1558,6 +1558,128 @@ def inner( return inner +@pytest.fixture +def streaming_responses_tool_call_model_responses(): + def inner( + tool_name: str, + arguments: str, + response_model: str, + response_text: str, + response_ids: "Iterator[str]", + usages: "Iterator[openai.types.responses.ResponseUsage]", + ): + first_id = next(response_ids) + second_id = next(response_ids) + first_usage = next(usages) + second_usage = next(usages) + + yield [ + openai.types.responses.ResponseCreatedEvent( + response=openai.types.responses.Response( + id=first_id, + output=[ + openai.types.responses.ResponseFunctionToolCall( + id="call_123", + call_id="call_123", + name=tool_name, + type="function_call", + arguments=arguments, + ) + ], + parallel_tool_calls=False, + tool_choice="none", + tools=[], + created_at=10000000, + model=response_model, + object="response", + usage=first_usage, + ), + sequence_number=0, + type="response.created", + ), + openai.types.responses.ResponseCompletedEvent( + response=openai.types.responses.Response( + id=first_id, + output=[ + openai.types.responses.ResponseFunctionToolCall( + id="call_123", + call_id="call_123", + name=tool_name, + type="function_call", + arguments=arguments, + ) + ], + parallel_tool_calls=False, + tool_choice="none", + tools=[], + created_at=10000000, + model=response_model, + object="response", + usage=first_usage, + ), + sequence_number=5, + type="response.completed", + ), + ] + + yield [ + openai.types.responses.ResponseCreatedEvent( + response=openai.types.responses.Response( + id=second_id, + output=[ + openai.types.responses.ResponseOutputMessage( + id="msg_final", + type="message", + status="in_progress", + content=[], + role="assistant", + ) + ], + parallel_tool_calls=False, + tool_choice="none", + tools=[], + created_at=10000000, + model=response_model, + object="response", + usage=second_usage, + ), + sequence_number=0, + type="response.created", + ), + openai.types.responses.ResponseCompletedEvent( + response=openai.types.responses.Response( + id=second_id, + output=[ + openai.types.responses.ResponseOutputMessage( + id="msg_final", + type="message", + status="completed", + content=[ + openai.types.responses.ResponseOutputText( + text=response_text, + type="output_text", + annotations=[], + ) + ], + role="assistant", + ) + ], + parallel_tool_calls=False, + tool_choice="none", + tools=[], + created_at=10000000, + model=response_model, + object="response", + usage=second_usage, + ), + sequence_number=7, + type="response.completed", + ), + ] + + return inner + + class MockServerRequestHandler(BaseHTTPRequestHandler): def do_GET(self): # noqa: N802 # Process an HTTP GET request and return a response. diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index 84ad453f90..71bda0b130 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -845,7 +845,7 @@ def test_tool_execution_span( send_default_pii, include_prompts, get_model_response, - responses_tool_call_model_responses, + nonstreaming_responses_tool_call_model_responses, stream_gen_ai_spans, span_streaming, ): @@ -862,7 +862,7 @@ def test_tool_execution_span( trace_lifecycle="stream" if span_streaming else "static", ) - responses = responses_tool_call_model_responses( + responses = nonstreaming_responses_tool_call_model_responses( tool_name="get_word_length", arguments='{"word": "eudca"}', response_model="gpt-4-0613", diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index 6b0aaea9f8..a27921a628 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -12,6 +12,7 @@ Agent, ModelResponse, ModelSettings, + RunHooks, Usage, ) from agents.computer import Computer @@ -2160,6 +2161,7 @@ async def test_max_turns_before_handoff_span( assert handoff_span["data"]["gen_ai.operation.name"] == "handoff" +@pytest.mark.parametrize("user_hooks", [True, False]) @pytest.mark.parametrize("span_streaming", [True, False]) @pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) @pytest.mark.asyncio @@ -2169,12 +2171,13 @@ async def test_tool_execution_span( capture_items, test_agent, get_model_response, - responses_tool_call_model_responses, + nonstreaming_responses_tool_call_model_responses, stream_gen_ai_spans, span_streaming, + user_hooks, ): """ - Test tool execution span creation. + Test tool execution span creation with `AgentRunner.run()`. """ @agents.function_tool @@ -2187,7 +2190,7 @@ def simple_test_tool(message: str) -> str: model = OpenAIResponsesModel(model="gpt-4", openai_client=client) agent_with_tool = test_agent.clone(tools=[simple_test_tool], model=model) - responses = responses_tool_call_model_responses( + responses = nonstreaming_responses_tool_call_model_responses( tool_name="simple_test_tool", arguments='{"message": "hello"}', response_model="gpt-4", @@ -2252,6 +2255,7 @@ def simple_test_tool(message: str) -> str: agent_with_tool, "Please use the simple test tool", run_config=test_run_config, + hooks=RunHooks() if user_hooks else None, ) sentry_sdk.flush() @@ -2451,6 +2455,7 @@ def simple_test_tool(message: str) -> str: agent_with_tool, "Please use the simple test tool", run_config=test_run_config, + hooks=RunHooks() if user_hooks else None, ) (transaction,) = (item.payload for item in items if item.type == "transaction") @@ -2644,6 +2649,7 @@ def simple_test_tool(message: str) -> str: agent_with_tool, "Please use the simple test tool", run_config=test_run_config, + hooks=RunHooks() if user_hooks else None, ) (transaction,) = events @@ -2783,6 +2789,242 @@ def simple_test_tool(message: str) -> str: assert ai_client_span2["data"]["gen_ai.usage.total_tokens"] == 25 +@pytest.mark.parametrize("user_hooks", [True, False]) +@pytest.mark.parametrize("span_streaming", [True, False]) +@pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) +@pytest.mark.asyncio +async def test_run_streamed_tool_execution_span( + sentry_init, + capture_events, + capture_items, + test_agent, + get_model_response, + async_iterator, + server_side_event_chunks, + streaming_responses_tool_call_model_responses, + stream_gen_ai_spans, + span_streaming, + user_hooks, +): + """ + Test tool execution span creation with `AgentRunner.run_streamed()`. + """ + + @agents.function_tool + def simple_test_tool(message: str) -> str: + """A simple tool""" + return f"Tool executed with: {message}" + + # Create agent with the tool + client = AsyncOpenAI(api_key="test-key") + model = OpenAIResponsesModel(model="gpt-4", openai_client=client) + agent_with_tool = test_agent.clone(tools=[simple_test_tool], model=model) + + responses = streaming_responses_tool_call_model_responses( + tool_name="simple_test_tool", + arguments='{"message": "hello"}', + response_model="gpt-4", + response_text="Task completed using the tool", + response_ids=iter(["resp_tool_123", "resp_final_123"]), + usages=iter( + [ + ResponseUsage( + input_tokens=10, + input_tokens_details=InputTokensDetails( + cached_tokens=0, + cache_write_tokens=0, + ), + output_tokens=5, + output_tokens_details=OutputTokensDetails( + reasoning_tokens=0, + ), + total_tokens=15, + ), + ResponseUsage( + input_tokens=15, + input_tokens_details=InputTokensDetails( + cached_tokens=0, + cache_write_tokens=0, + ), + output_tokens=10, + output_tokens_details=OutputTokensDetails( + reasoning_tokens=0, + ), + total_tokens=25, + ), + ] + ), + ) + + request_headers = {} + # openai-agents calls with_streaming_response() if available starting with + # https://github.com/openai/openai-agents-python/commit/159beb56130f7d85192acfd593c9168757984dc0. + # When using with_streaming_response() the header set below changes the response type: + # https://github.com/openai/openai-python/blob/656e3cab4a18262a49b961d41293367e45ee71b9/src/openai/_response.py#L67. + if parse_version(OPENAI_AGENTS_VERSION) >= (0, 10, 3) and hasattr( + agent_with_tool.model._client.responses, "with_streaming_response" + ): + request_headers["X-Stainless-Raw-Response"] = "stream" + + tool_response = get_model_response( + async_iterator(server_side_event_chunks(next(responses))), + request_headers=request_headers, + ) + final_response = get_model_response( + async_iterator(server_side_event_chunks(next(responses))), + request_headers=request_headers, + ) + + if span_streaming: + with patch.object( + agent_with_tool.model._client._client, + "send", + side_effect=[tool_response, final_response], + ) as _: + sentry_init( + integrations=[OpenAIAgentsIntegration()], + disabled_integrations=[StdlibIntegration], + traces_sample_rate=1.0, + send_default_pii=True, + stream_gen_ai_spans=stream_gen_ai_spans, + trace_lifecycle="stream", + ) + + items = capture_items("span") + + result = agents.Runner.run_streamed( + agent_with_tool, + "Please use the simple test tool", + run_config=test_run_config, + hooks=RunHooks() if user_hooks else None, + ) + + async for event in result.stream_events(): + pass + + sentry_sdk.flush() + spans = [item.payload for item in items] + + tool_span = next( + span + for span in spans + if span["attributes"].get("sentry.op") == OP.GEN_AI_EXECUTE_TOOL + ) + + assert tool_span["name"] == "execute_tool simple_test_tool" + assert tool_span["attributes"]["gen_ai.agent.name"] == "test_agent" + assert tool_span["attributes"]["gen_ai.operation.name"] == "execute_tool" + + assert tool_span["attributes"]["gen_ai.request.max_tokens"] == 100 + assert tool_span["attributes"]["gen_ai.request.model"] == "gpt-4" + assert tool_span["attributes"]["gen_ai.request.temperature"] == 0.7 + assert tool_span["attributes"]["gen_ai.request.top_p"] == 1.0 + assert tool_span["attributes"]["gen_ai.system"] == "openai" + assert tool_span["attributes"]["gen_ai.tool.description"] == "A simple tool" + assert tool_span["attributes"]["gen_ai.tool.input"] == '{"message": "hello"}' + assert tool_span["attributes"]["gen_ai.tool.name"] == "simple_test_tool" + assert ( + tool_span["attributes"]["gen_ai.tool.output"] == "Tool executed with: hello" + ) + + elif stream_gen_ai_spans: + with patch.object( + agent_with_tool.model._client._client, + "send", + side_effect=[tool_response, final_response], + ) as _: + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + send_default_pii=True, + stream_gen_ai_spans=stream_gen_ai_spans, + ) + + items = capture_items("transaction", "span") + + result = agents.Runner.run_streamed( + agent_with_tool, + "Please use the simple test tool", + run_config=test_run_config, + hooks=RunHooks() if user_hooks else None, + ) + + async for event in result.stream_events(): + pass + + (transaction,) = (item.payload for item in items if item.type == "transaction") + assert transaction["transaction"] == "test_agent workflow" + assert transaction["contexts"]["trace"]["origin"] == "auto.ai.openai_agents" + + spans = [item.payload for item in items if item.type == "span"] + tool_span = next( + span + for span in spans + if span["attributes"]["sentry.op"] == OP.GEN_AI_EXECUTE_TOOL + ) + + assert tool_span["name"] == "execute_tool simple_test_tool" + assert tool_span["attributes"]["gen_ai.agent.name"] == "test_agent" + assert tool_span["attributes"]["gen_ai.operation.name"] == "execute_tool" + + assert tool_span["attributes"]["gen_ai.request.max_tokens"] == 100 + assert tool_span["attributes"]["gen_ai.request.model"] == "gpt-4" + assert tool_span["attributes"]["gen_ai.request.temperature"] == 0.7 + assert tool_span["attributes"]["gen_ai.request.top_p"] == 1.0 + assert tool_span["attributes"]["gen_ai.system"] == "openai" + assert tool_span["attributes"]["gen_ai.tool.description"] == "A simple tool" + assert tool_span["attributes"]["gen_ai.tool.input"] == '{"message": "hello"}' + assert tool_span["attributes"]["gen_ai.tool.name"] == "simple_test_tool" + assert ( + tool_span["attributes"]["gen_ai.tool.output"] == "Tool executed with: hello" + ) + else: + with patch.object( + agent_with_tool.model._client._client, + "send", + side_effect=[tool_response, final_response], + ) as _: + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + send_default_pii=True, + stream_gen_ai_spans=stream_gen_ai_spans, + ) + + events = capture_events() + + result = agents.Runner.run_streamed( + agent_with_tool, + "Please use the simple test tool", + run_config=test_run_config, + hooks=RunHooks() if user_hooks else None, + ) + + async for event in result.stream_events(): + pass + + (transaction,) = events + spans = transaction["spans"] + tool_span = next(span for span in spans if span["op"] == OP.GEN_AI_EXECUTE_TOOL) + + assert transaction["transaction"] == "test_agent workflow" + assert transaction["contexts"]["trace"]["origin"] == "auto.ai.openai_agents" + + assert tool_span["description"] == "execute_tool simple_test_tool" + assert tool_span["data"]["gen_ai.agent.name"] == "test_agent" + assert tool_span["data"]["gen_ai.operation.name"] == "execute_tool" + + assert tool_span["data"]["gen_ai.request.max_tokens"] == 100 + assert tool_span["data"]["gen_ai.request.model"] == "gpt-4" + assert tool_span["data"]["gen_ai.request.temperature"] == 0.7 + assert tool_span["data"]["gen_ai.request.top_p"] == 1.0 + assert tool_span["data"]["gen_ai.system"] == "openai" + assert tool_span["data"]["gen_ai.tool.description"] == "A simple tool" + assert tool_span["data"]["gen_ai.tool.input"] == '{"message": "hello"}' + assert tool_span["data"]["gen_ai.tool.name"] == "simple_test_tool" + assert tool_span["data"]["gen_ai.tool.output"] == "Tool executed with: hello" + + @pytest.mark.asyncio async def test_hosted_mcp_tool_propagation_header_streamed( sentry_init, @@ -3782,7 +4024,7 @@ async def test_tool_execution_error_tracing( capture_items, test_agent, get_model_response, - responses_tool_call_model_responses, + nonstreaming_responses_tool_call_model_responses, stream_gen_ai_spans, span_streaming, ): @@ -3807,7 +4049,7 @@ def failing_tool(message: str) -> str: model = OpenAIResponsesModel(model="gpt-4", openai_client=client) agent_with_tool = test_agent.clone(tools=[failing_tool], model=model) - responses = responses_tool_call_model_responses( + responses = nonstreaming_responses_tool_call_model_responses( tool_name="failing_tool", arguments='{"message": "test"}', response_model="gpt-4-0613",