From 0ef6ac664a3ad0e315f29f3d45ea7a27d5384236 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 25 Jun 2025 15:15:01 +0200 Subject: [PATCH 1/2] set_data to set_attribute --- .../openai_agents/spans/ai_client.py | 2 +- .../openai_agents/spans/execute_tool.py | 12 +++---- .../openai_agents/spans/handoff.py | 2 +- .../openai_agents/spans/invoke_agent.py | 2 +- .../integrations/openai_agents/utils.py | 36 ++++++++++--------- 5 files changed, 28 insertions(+), 26 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/spans/ai_client.py b/sentry_sdk/integrations/openai_agents/spans/ai_client.py index 30c5fd1dac..67b3e77033 100644 --- a/sentry_sdk/integrations/openai_agents/spans/ai_client.py +++ b/sentry_sdk/integrations/openai_agents/spans/ai_client.py @@ -25,7 +25,7 @@ def ai_client_span(agent, get_response_kwargs): origin=SPAN_ORIGIN, ) # TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + span.set_attribute(SPANDATA.GEN_AI_OPERATION_NAME, "chat") return span diff --git a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/sentry_sdk/integrations/openai_agents/spans/execute_tool.py index e6e880b64c..e1e9fed7aa 100644 --- a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py +++ b/sentry_sdk/integrations/openai_agents/spans/execute_tool.py @@ -20,17 +20,17 @@ def execute_tool_span(tool, *args, **kwargs): origin=SPAN_ORIGIN, ) - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") + span.set_attribute(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") if tool.__class__.__name__ == "FunctionTool": - span.set_data(SPANDATA.GEN_AI_TOOL_TYPE, "function") + span.set_attribute(SPANDATA.GEN_AI_TOOL_TYPE, "function") - span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name) - span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description) + span.set_attribute(SPANDATA.GEN_AI_TOOL_NAME, tool.name) + span.set_attribute(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description) if should_send_default_pii(): input = args[1] - span.set_data(SPANDATA.GEN_AI_TOOL_INPUT, input) + span.set_attribute(SPANDATA.GEN_AI_TOOL_INPUT, input) return span @@ -40,4 +40,4 @@ def update_execute_tool_span(span, agent, tool, result): _set_agent_data(span, agent) if should_send_default_pii(): - span.set_data(SPANDATA.GEN_AI_TOOL_OUTPUT, result) + span.set_attribute(SPANDATA.GEN_AI_TOOL_OUTPUT, result) diff --git a/sentry_sdk/integrations/openai_agents/spans/handoff.py b/sentry_sdk/integrations/openai_agents/spans/handoff.py index 78e6788c7d..96672ea7df 100644 --- a/sentry_sdk/integrations/openai_agents/spans/handoff.py +++ b/sentry_sdk/integrations/openai_agents/spans/handoff.py @@ -16,4 +16,4 @@ def handoff_span(context, from_agent, to_agent_name): name=f"handoff from {from_agent.name} to {to_agent_name}", origin=SPAN_ORIGIN, ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "handoff") + span.set_attribute(SPANDATA.GEN_AI_OPERATION_NAME, "handoff") diff --git a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py index 549ade1246..1d88d6fc7b 100644 --- a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -20,7 +20,7 @@ def invoke_agent_span(context, agent): ) span.__enter__() - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + span.set_attribute(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") _set_agent_data(span, agent) diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index 56689fe336..58330fe4a3 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -31,42 +31,42 @@ def _capture_exception(exc): def _set_agent_data(span, agent): # type: (sentry_sdk.tracing.Span, agents.Agent) -> None - span.set_data( + span.set_attribute( SPANDATA.GEN_AI_SYSTEM, "openai" ) # See footnote for https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-system for explanation why. - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent.name) + span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, agent.name) if agent.model_settings.max_tokens: - span.set_data( + span.set_attribute( SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, agent.model_settings.max_tokens ) if agent.model: - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, agent.model) + span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, agent.model) if agent.model_settings.presence_penalty: - span.set_data( + span.set_attribute( SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, agent.model_settings.presence_penalty, ) if agent.model_settings.temperature: - span.set_data( + span.set_attribute( SPANDATA.GEN_AI_REQUEST_TEMPERATURE, agent.model_settings.temperature ) if agent.model_settings.top_p: - span.set_data(SPANDATA.GEN_AI_REQUEST_TOP_P, agent.model_settings.top_p) + span.set_attribute(SPANDATA.GEN_AI_REQUEST_TOP_P, agent.model_settings.top_p) if agent.model_settings.frequency_penalty: - span.set_data( + span.set_attribute( SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, agent.model_settings.frequency_penalty, ) if len(agent.tools) > 0: - span.set_data( + span.set_attribute( SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize([vars(tool) for tool in agent.tools]), ) @@ -74,17 +74,17 @@ def _set_agent_data(span, agent): def _set_usage_data(span, usage): # type: (sentry_sdk.tracing.Span, Usage) -> None - span.set_data(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) - span.set_data( + span.set_attribute(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) + span.set_attribute( SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, usage.input_tokens_details.cached_tokens, ) - span.set_data(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) - span.set_data( + span.set_attribute(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) + span.set_attribute( SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, usage.output_tokens_details.reasoning_tokens, ) - span.set_data(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) + span.set_attribute(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) def _set_input_data(span, get_response_kwargs): @@ -118,7 +118,9 @@ def _set_input_data(span, get_response_kwargs): if len(messages) > 0: request_messages.append({"role": role, "content": messages}) - span.set_data(SPANDATA.GEN_AI_REQUEST_MESSAGES, safe_serialize(request_messages)) + span.set_attribute( + SPANDATA.GEN_AI_REQUEST_MESSAGES, safe_serialize(request_messages) + ) def _set_output_data(span, result): @@ -143,12 +145,12 @@ def _set_output_data(span, result): output_messages["response"].append(output_message.dict()) if len(output_messages["tool"]) > 0: - span.set_data( + span.set_attribute( SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(output_messages["tool"]) ) if len(output_messages["response"]) > 0: - span.set_data( + span.set_attribute( SPANDATA.GEN_AI_RESPONSE_TEXT, safe_serialize(output_messages["response"]) ) From 40ffeef460e213f29eea4a6b8415caaaba3980b3 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 25 Jun 2025 15:34:00 +0200 Subject: [PATCH 2/2] removed openai-agents from potel --- .github/workflows/test-integrations-ai.yml | 8 - scripts/populate_tox/config.py | 6 - scripts/populate_tox/tox.jinja | 1 - .../split_tox_gh_actions.py | 1 - sentry_sdk/integrations/__init__.py | 1 - .../integrations/openai_agents/__init__.py | 53 -- .../integrations/openai_agents/consts.py | 1 - .../openai_agents/patches/__init__.py | 4 - .../openai_agents/patches/agent_run.py | 143 ----- .../openai_agents/patches/models.py | 50 -- .../openai_agents/patches/runner.py | 42 -- .../openai_agents/patches/tools.py | 77 --- .../openai_agents/spans/__init__.py | 5 - .../openai_agents/spans/agent_workflow.py | 20 - .../openai_agents/spans/ai_client.py | 38 -- .../openai_agents/spans/execute_tool.py | 43 -- .../openai_agents/spans/handoff.py | 19 - .../openai_agents/spans/invoke_agent.py | 34 - .../integrations/openai_agents/utils.py | 201 ------ tests/integrations/openai_agents/__init__.py | 3 - .../openai_agents/test_openai_agents.py | 580 ------------------ tox.ini | 12 +- 22 files changed, 3 insertions(+), 1339 deletions(-) delete mode 100644 sentry_sdk/integrations/openai_agents/__init__.py delete mode 100644 sentry_sdk/integrations/openai_agents/consts.py delete mode 100644 sentry_sdk/integrations/openai_agents/patches/__init__.py delete mode 100644 sentry_sdk/integrations/openai_agents/patches/agent_run.py delete mode 100644 sentry_sdk/integrations/openai_agents/patches/models.py delete mode 100644 sentry_sdk/integrations/openai_agents/patches/runner.py delete mode 100644 sentry_sdk/integrations/openai_agents/patches/tools.py delete mode 100644 sentry_sdk/integrations/openai_agents/spans/__init__.py delete mode 100644 sentry_sdk/integrations/openai_agents/spans/agent_workflow.py delete mode 100644 sentry_sdk/integrations/openai_agents/spans/ai_client.py delete mode 100644 sentry_sdk/integrations/openai_agents/spans/execute_tool.py delete mode 100644 sentry_sdk/integrations/openai_agents/spans/handoff.py delete mode 100644 sentry_sdk/integrations/openai_agents/spans/invoke_agent.py delete mode 100644 sentry_sdk/integrations/openai_agents/utils.py delete mode 100644 tests/integrations/openai_agents/__init__.py delete mode 100644 tests/integrations/openai_agents/test_openai_agents.py diff --git a/.github/workflows/test-integrations-ai.yml b/.github/workflows/test-integrations-ai.yml index 5b31a20a35..99f59df833 100644 --- a/.github/workflows/test-integrations-ai.yml +++ b/.github/workflows/test-integrations-ai.yml @@ -59,10 +59,6 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh "py${{ matrix.python-version }}-openai-latest" - - name: Test openai_agents latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-openai_agents-latest" - name: Test huggingface_hub latest run: | set -x # print commands that are executed @@ -125,10 +121,6 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openai" - - name: Test openai_agents pinned - run: | - set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openai_agents" - name: Test huggingface_hub pinned run: | set -x # print commands that are executed diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 7e48d620c0..62e916e5bd 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -152,12 +152,6 @@ "loguru": { "package": "loguru", }, - "openai_agents": { - "package": "openai-agents", - "deps": { - "*": ["pytest-asyncio"], - }, - }, "openfeature": { "package": "openfeature-sdk", }, diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 3ef70b3041..32b6cadbfa 100644 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -371,7 +371,6 @@ setenv = litestar: TESTPATH=tests/integrations/litestar loguru: TESTPATH=tests/integrations/loguru openai: TESTPATH=tests/integrations/openai - openai_agents: TESTPATH=tests/integrations/openai_agents openfeature: TESTPATH=tests/integrations/openfeature pure_eval: TESTPATH=tests/integrations/pure_eval pymongo: TESTPATH=tests/integrations/pymongo diff --git a/scripts/split_tox_gh_actions/split_tox_gh_actions.py b/scripts/split_tox_gh_actions/split_tox_gh_actions.py index af1ff84cd6..3fbc0ec1c5 100755 --- a/scripts/split_tox_gh_actions/split_tox_gh_actions.py +++ b/scripts/split_tox_gh_actions/split_tox_gh_actions.py @@ -63,7 +63,6 @@ "cohere", "langchain", "openai", - "openai_agents", "huggingface_hub", ], "Cloud": [ diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index a480c86fcf..f2d1a28522 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -146,7 +146,6 @@ def iter_default_integrations(with_auto_enabling_integrations): "launchdarkly": (9, 8, 0), "loguru": (0, 7, 0), "openai": (1, 0, 0), - "openai_agents": (0, 0, 19), "openfeature": (0, 7, 1), "quart": (0, 16, 0), "ray": (2, 7, 0), diff --git a/sentry_sdk/integrations/openai_agents/__init__.py b/sentry_sdk/integrations/openai_agents/__init__.py deleted file mode 100644 index 06b6459441..0000000000 --- a/sentry_sdk/integrations/openai_agents/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -from sentry_sdk.integrations import DidNotEnable, Integration - -from .patches import ( - _create_get_model_wrapper, - _create_get_all_tools_wrapper, - _create_run_wrapper, - _patch_agent_run, -) - -try: - import agents - -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _patch_runner(): - # type: () -> 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. - # TODO-anton: Also patch streaming runner: agents.Runner.run_streamed - agents.run.DEFAULT_AGENT_RUNNER.run = _create_run_wrapper( - agents.run.DEFAULT_AGENT_RUNNER.run - ) - - # Creating the actual spans for each agent run. - _patch_agent_run() - - -def _patch_model(): - # type: () -> None - agents.run.AgentRunner._get_model = classmethod( - _create_get_model_wrapper(agents.run.AgentRunner._get_model), - ) - - -def _patch_tools(): - # type: () -> None - agents.run.AgentRunner._get_all_tools = classmethod( - _create_get_all_tools_wrapper(agents.run.AgentRunner._get_all_tools), - ) - - -class OpenAIAgentsIntegration(Integration): - identifier = "openai_agents" - - @staticmethod - def setup_once(): - # type: () -> None - _patch_tools() - _patch_model() - _patch_runner() diff --git a/sentry_sdk/integrations/openai_agents/consts.py b/sentry_sdk/integrations/openai_agents/consts.py deleted file mode 100644 index f5de978be0..0000000000 --- a/sentry_sdk/integrations/openai_agents/consts.py +++ /dev/null @@ -1 +0,0 @@ -SPAN_ORIGIN = "auto.ai.openai_agents" diff --git a/sentry_sdk/integrations/openai_agents/patches/__init__.py b/sentry_sdk/integrations/openai_agents/patches/__init__.py deleted file mode 100644 index 06bb1711f8..0000000000 --- a/sentry_sdk/integrations/openai_agents/patches/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .models import _create_get_model_wrapper # noqa: F401 -from .tools import _create_get_all_tools_wrapper # noqa: F401 -from .runner import _create_run_wrapper # noqa: F401 -from .agent_run import _patch_agent_run # noqa: F401 diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py deleted file mode 100644 index 084100878c..0000000000 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ /dev/null @@ -1,143 +0,0 @@ -from functools import wraps - -from sentry_sdk.integrations import DidNotEnable - -from ..spans import invoke_agent_span, update_invoke_agent_span, handoff_span - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Optional - - -try: - import agents -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _patch_agent_run(): - # type: () -> None - """ - Patches AgentRunner methods to create agent invocation spans. - This directly patches the execution flow to track when agents start and stop. - """ - - # Store original methods - original_run_single_turn = agents.run.AgentRunner._run_single_turn - original_execute_handoffs = agents._run_impl.RunImpl.execute_handoffs - original_execute_final_output = agents._run_impl.RunImpl.execute_final_output - - def _start_invoke_agent_span(context_wrapper, agent): - # type: (agents.RunContextWrapper, agents.Agent) -> None - """Start an agent invocation span""" - # Store the agent on the context wrapper so we can access it later - context_wrapper._sentry_current_agent = agent - invoke_agent_span(context_wrapper, agent) - - def _end_invoke_agent_span(context_wrapper, agent, output=None): - # type: (agents.RunContextWrapper, agents.Agent, Optional[Any]) -> None - """End the agent invocation span""" - # Clear the stored agent - if hasattr(context_wrapper, "_sentry_current_agent"): - delattr(context_wrapper, "_sentry_current_agent") - - update_invoke_agent_span(context_wrapper, agent, output) - - def _has_active_agent_span(context_wrapper): - # type: (agents.RunContextWrapper) -> bool - """Check if there's an active agent span for this context""" - return getattr(context_wrapper, "_sentry_current_agent", None) is not None - - def _get_current_agent(context_wrapper): - # type: (agents.RunContextWrapper) -> Optional[agents.Agent] - """Get the current agent from context wrapper""" - return getattr(context_wrapper, "_sentry_current_agent", None) - - @wraps( - original_run_single_turn.__func__ - if hasattr(original_run_single_turn, "__func__") - else original_run_single_turn - ) - async def patched_run_single_turn(cls, *args, **kwargs): - # type: (agents.Runner, *Any, **Any) -> Any - """Patched _run_single_turn that creates agent invocation spans""" - - agent = kwargs.get("agent") - context_wrapper = kwargs.get("context_wrapper") - should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks") - - # Start agent span when agent starts (but only once per agent) - if should_run_agent_start_hooks and agent and context_wrapper: - # End any existing span for a different agent - if _has_active_agent_span(context_wrapper): - current_agent = _get_current_agent(context_wrapper) - if current_agent and current_agent != agent: - _end_invoke_agent_span(context_wrapper, current_agent) - - _start_invoke_agent_span(context_wrapper, agent) - - # Call original method with all the correct parameters - result = await original_run_single_turn(*args, **kwargs) - - return result - - @wraps( - original_execute_handoffs.__func__ - if hasattr(original_execute_handoffs, "__func__") - else original_execute_handoffs - ) - async def patched_execute_handoffs(cls, *args, **kwargs): - # type: (agents.Runner, *Any, **Any) -> Any - """Patched execute_handoffs that creates handoff spans and ends agent span for handoffs""" - - context_wrapper = kwargs.get("context_wrapper") - run_handoffs = kwargs.get("run_handoffs") - agent = kwargs.get("agent") - - # Create Sentry handoff span for the first handoff (agents library only processes the first one) - if run_handoffs: - first_handoff = run_handoffs[0] - handoff_agent_name = first_handoff.handoff.agent_name - handoff_span(context_wrapper, agent, handoff_agent_name) - - # Call original method with all parameters - try: - result = await original_execute_handoffs(*args, **kwargs) - - finally: - # End span for current agent after handoff processing is complete - if agent and context_wrapper and _has_active_agent_span(context_wrapper): - _end_invoke_agent_span(context_wrapper, agent) - - return result - - @wraps( - original_execute_final_output.__func__ - if hasattr(original_execute_final_output, "__func__") - else original_execute_final_output - ) - async def patched_execute_final_output(cls, *args, **kwargs): - # type: (agents.Runner, *Any, **Any) -> Any - """Patched execute_final_output that ends agent span for final outputs""" - - agent = kwargs.get("agent") - context_wrapper = kwargs.get("context_wrapper") - final_output = kwargs.get("final_output") - - # Call original method with all parameters - try: - result = await original_execute_final_output(*args, **kwargs) - finally: - # End span for current agent after final output processing is complete - if agent and context_wrapper and _has_active_agent_span(context_wrapper): - _end_invoke_agent_span(context_wrapper, agent, final_output) - - return result - - # Apply patches - agents.run.AgentRunner._run_single_turn = classmethod(patched_run_single_turn) - agents._run_impl.RunImpl.execute_handoffs = classmethod(patched_execute_handoffs) - agents._run_impl.RunImpl.execute_final_output = classmethod( - patched_execute_final_output - ) diff --git a/sentry_sdk/integrations/openai_agents/patches/models.py b/sentry_sdk/integrations/openai_agents/patches/models.py deleted file mode 100644 index e6f24da6a1..0000000000 --- a/sentry_sdk/integrations/openai_agents/patches/models.py +++ /dev/null @@ -1,50 +0,0 @@ -from functools import wraps - -from sentry_sdk.integrations import DidNotEnable - -from ..spans import ai_client_span, update_ai_client_span - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable - - -try: - import agents -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _create_get_model_wrapper(original_get_model): - # type: (Callable[..., Any]) -> Callable[..., Any] - """ - Wraps the agents.Runner._get_model method to wrap the get_response method of the model to create a AI client span. - """ - - @wraps( - original_get_model.__func__ - if hasattr(original_get_model, "__func__") - else original_get_model - ) - def wrapped_get_model(cls, agent, run_config): - # type: (agents.Runner, agents.Agent, agents.RunConfig) -> agents.Model - - model = original_get_model(agent, run_config) - original_get_response = model.get_response - - @wraps(original_get_response) - async def wrapped_get_response(*args, **kwargs): - # type: (*Any, **Any) -> Any - with ai_client_span(agent, kwargs) as span: - result = await original_get_response(*args, **kwargs) - - update_ai_client_span(span, agent, kwargs, result) - - return result - - model.get_response = wrapped_get_response - - return model - - return wrapped_get_model diff --git a/sentry_sdk/integrations/openai_agents/patches/runner.py b/sentry_sdk/integrations/openai_agents/patches/runner.py deleted file mode 100644 index e1e9a3b50c..0000000000 --- a/sentry_sdk/integrations/openai_agents/patches/runner.py +++ /dev/null @@ -1,42 +0,0 @@ -from functools import wraps - -import sentry_sdk - -from ..spans import agent_workflow_span -from ..utils import _capture_exception - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable - - -def _create_run_wrapper(original_func): - # type: (Callable[..., Any]) -> Callable[..., Any] - """ - Wraps the agents.Runner.run methods to create a root span for the agent workflow runs. - - Note agents.Runner.run_sync() is a wrapper around agents.Runner.run(), - so it does not need to be wrapped separately. - """ - - @wraps(original_func) - async def wrapper(*args, **kwargs): - # type: (*Any, **Any) -> Any - agent = args[0] - with agent_workflow_span(agent): - result = None - try: - result = await original_func(*args, **kwargs) - return result - except Exception as exc: - _capture_exception(exc) - - # It could be that there is a "invoke agent" span still open - current_span = sentry_sdk.get_current_span() - if current_span is not None and current_span.timestamp is None: - current_span.__exit__(None, None, None) - - raise exc from None - - return wrapper diff --git a/sentry_sdk/integrations/openai_agents/patches/tools.py b/sentry_sdk/integrations/openai_agents/patches/tools.py deleted file mode 100644 index b359d32678..0000000000 --- a/sentry_sdk/integrations/openai_agents/patches/tools.py +++ /dev/null @@ -1,77 +0,0 @@ -from functools import wraps - -from sentry_sdk.integrations import DidNotEnable - -from ..spans import execute_tool_span, update_execute_tool_span - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable - -try: - import agents -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _create_get_all_tools_wrapper(original_get_all_tools): - # type: (Callable[..., Any]) -> Callable[..., Any] - """ - Wraps the agents.Runner._get_all_tools method of the Runner class to wrap all function tools with Sentry instrumentation. - """ - - @wraps( - original_get_all_tools.__func__ - if hasattr(original_get_all_tools, "__func__") - else original_get_all_tools - ) - async def wrapped_get_all_tools(cls, agent, context_wrapper): - # type: (agents.Runner, agents.Agent, agents.RunContextWrapper) -> list[agents.Tool] - - # Get the original tools - tools = await original_get_all_tools(agent, context_wrapper) - - wrapped_tools = [] - for tool in tools: - # Wrap only the function tools (for now) - if tool.__class__.__name__ != "FunctionTool": - wrapped_tools.append(tool) - continue - - # Create a new FunctionTool with our wrapped invoke method - original_on_invoke = tool.on_invoke_tool - - def create_wrapped_invoke(current_tool, current_on_invoke): - # type: (agents.Tool, Callable[..., Any]) -> Callable[..., Any] - @wraps(current_on_invoke) - async def sentry_wrapped_on_invoke_tool(*args, **kwargs): - # type: (*Any, **Any) -> Any - with execute_tool_span(current_tool, *args, **kwargs) as span: - # We can not capture exceptions in tool execution here because - # `_on_invoke_tool` is swallowing the exception here: - # https://github.com/openai/openai-agents-python/blob/main/src/agents/tool.py#L409-L422 - # And because function_tool is a decorator with `default_tool_error_function` set as a default parameter - # I was unable to monkey patch it because those are evaluated at module import time - # and the SDK is too late to patch it. I was also unable to patch `_on_invoke_tool_impl` - # because it is nested inside this import time code. As if they made it hard to patch on purpose... - result = await current_on_invoke(*args, **kwargs) - update_execute_tool_span(span, agent, current_tool, result) - - return result - - return sentry_wrapped_on_invoke_tool - - wrapped_tool = agents.FunctionTool( - name=tool.name, - description=tool.description, - params_json_schema=tool.params_json_schema, - on_invoke_tool=create_wrapped_invoke(tool, original_on_invoke), - strict_json_schema=tool.strict_json_schema, - is_enabled=tool.is_enabled, - ) - wrapped_tools.append(wrapped_tool) - - return wrapped_tools - - return wrapped_get_all_tools diff --git a/sentry_sdk/integrations/openai_agents/spans/__init__.py b/sentry_sdk/integrations/openai_agents/spans/__init__.py deleted file mode 100644 index 3bc453cafa..0000000000 --- a/sentry_sdk/integrations/openai_agents/spans/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .agent_workflow import agent_workflow_span # noqa: F401 -from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 -from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 -from .handoff import handoff_span # noqa: F401 -from .invoke_agent import invoke_agent_span, update_invoke_agent_span # noqa: F401 diff --git a/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py b/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py deleted file mode 100644 index f43d3b92d6..0000000000 --- a/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +++ /dev/null @@ -1,20 +0,0 @@ -import sentry_sdk - -from ..consts import SPAN_ORIGIN - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - import agents - - -def agent_workflow_span(agent): - # type: (agents.Agent) -> sentry_sdk.tracing.Span - - # Create a transaction or a span if an transaction is already active - span = sentry_sdk.start_span( - name=f"{agent.name} workflow", - origin=SPAN_ORIGIN, - ) - - return span diff --git a/sentry_sdk/integrations/openai_agents/spans/ai_client.py b/sentry_sdk/integrations/openai_agents/spans/ai_client.py deleted file mode 100644 index 67b3e77033..0000000000 --- a/sentry_sdk/integrations/openai_agents/spans/ai_client.py +++ /dev/null @@ -1,38 +0,0 @@ -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA - -from ..consts import SPAN_ORIGIN -from ..utils import ( - _set_agent_data, - _set_input_data, - _set_output_data, - _set_usage_data, -) - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from agents import Agent - from typing import Any - - -def ai_client_span(agent, get_response_kwargs): - # type: (Agent, dict[str, Any]) -> sentry_sdk.tracing.Span - # TODO-anton: implement other types of operations. Now "chat" is hardcoded. - span = sentry_sdk.start_span( - op=OP.GEN_AI_CHAT, - description=f"chat {agent.model}", - origin=SPAN_ORIGIN, - ) - # TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on - span.set_attribute(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - - return span - - -def update_ai_client_span(span, agent, get_response_kwargs, result): - # type: (sentry_sdk.tracing.Span, Agent, dict[str, Any], Any) -> None - _set_agent_data(span, agent) - _set_usage_data(span, result.usage) - _set_input_data(span, get_response_kwargs) - _set_output_data(span, result) diff --git a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/sentry_sdk/integrations/openai_agents/spans/execute_tool.py deleted file mode 100644 index e1e9fed7aa..0000000000 --- a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py +++ /dev/null @@ -1,43 +0,0 @@ -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.scope import should_send_default_pii - -from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - import agents - from typing import Any - - -def execute_tool_span(tool, *args, **kwargs): - # type: (agents.Tool, *Any, **Any) -> sentry_sdk.tracing.Span - span = sentry_sdk.start_span( - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool.name}", - origin=SPAN_ORIGIN, - ) - - span.set_attribute(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") - - if tool.__class__.__name__ == "FunctionTool": - span.set_attribute(SPANDATA.GEN_AI_TOOL_TYPE, "function") - - span.set_attribute(SPANDATA.GEN_AI_TOOL_NAME, tool.name) - span.set_attribute(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description) - - if should_send_default_pii(): - input = args[1] - span.set_attribute(SPANDATA.GEN_AI_TOOL_INPUT, input) - - return span - - -def update_execute_tool_span(span, agent, tool, result): - # type: (sentry_sdk.tracing.Span, agents.Agent, agents.Tool, Any) -> None - _set_agent_data(span, agent) - - if should_send_default_pii(): - span.set_attribute(SPANDATA.GEN_AI_TOOL_OUTPUT, result) diff --git a/sentry_sdk/integrations/openai_agents/spans/handoff.py b/sentry_sdk/integrations/openai_agents/spans/handoff.py deleted file mode 100644 index 96672ea7df..0000000000 --- a/sentry_sdk/integrations/openai_agents/spans/handoff.py +++ /dev/null @@ -1,19 +0,0 @@ -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA - -from ..consts import SPAN_ORIGIN - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - import agents - - -def handoff_span(context, from_agent, to_agent_name): - # type: (agents.RunContextWrapper, agents.Agent, str) -> None - with sentry_sdk.start_span( - op=OP.GEN_AI_HANDOFF, - name=f"handoff from {from_agent.name} to {to_agent_name}", - origin=SPAN_ORIGIN, - ) as span: - span.set_attribute(SPANDATA.GEN_AI_OPERATION_NAME, "handoff") diff --git a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py deleted file mode 100644 index 1d88d6fc7b..0000000000 --- a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ /dev/null @@ -1,34 +0,0 @@ -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA - -from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - import agents - from typing import Any - - -def invoke_agent_span(context, agent): - # type: (agents.RunContextWrapper, agents.Agent) -> sentry_sdk.tracing.Span - span = sentry_sdk.start_span( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {agent.name}", - origin=SPAN_ORIGIN, - ) - span.__enter__() - - span.set_attribute(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - _set_agent_data(span, agent) - - return span - - -def update_invoke_agent_span(context, agent, output): - # type: (agents.RunContextWrapper, agents.Agent, Any) -> None - current_span = sentry_sdk.get_current_span() - if current_span: - current_span.__exit__(None, None, None) diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py deleted file mode 100644 index 58330fe4a3..0000000000 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ /dev/null @@ -1,201 +0,0 @@ -import json -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.utils import event_from_exception - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - from typing import Union - from agents import Usage - -try: - import agents - -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _capture_exception(exc): - # type: (Any) -> None - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "openai_agents", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _set_agent_data(span, agent): - # type: (sentry_sdk.tracing.Span, agents.Agent) -> None - span.set_attribute( - SPANDATA.GEN_AI_SYSTEM, "openai" - ) # See footnote for https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-system for explanation why. - - span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, agent.name) - - if agent.model_settings.max_tokens: - span.set_attribute( - SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, agent.model_settings.max_tokens - ) - - if agent.model: - span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, agent.model) - - if agent.model_settings.presence_penalty: - span.set_attribute( - SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - agent.model_settings.presence_penalty, - ) - - if agent.model_settings.temperature: - span.set_attribute( - SPANDATA.GEN_AI_REQUEST_TEMPERATURE, agent.model_settings.temperature - ) - - if agent.model_settings.top_p: - span.set_attribute(SPANDATA.GEN_AI_REQUEST_TOP_P, agent.model_settings.top_p) - - if agent.model_settings.frequency_penalty: - span.set_attribute( - SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - agent.model_settings.frequency_penalty, - ) - - if len(agent.tools) > 0: - span.set_attribute( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - safe_serialize([vars(tool) for tool in agent.tools]), - ) - - -def _set_usage_data(span, usage): - # type: (sentry_sdk.tracing.Span, Usage) -> None - span.set_attribute(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) - span.set_attribute( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - usage.input_tokens_details.cached_tokens, - ) - span.set_attribute(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) - span.set_attribute( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - usage.output_tokens_details.reasoning_tokens, - ) - span.set_attribute(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) - - -def _set_input_data(span, get_response_kwargs): - # type: (sentry_sdk.tracing.Span, dict[str, Any]) -> None - if not should_send_default_pii(): - return - - messages_by_role = { - "system": [], - "user": [], - "assistant": [], - "tool": [], - } # type: (dict[str, list[Any]]) - system_instructions = get_response_kwargs.get("system_instructions") - if system_instructions: - messages_by_role["system"].append({"type": "text", "text": system_instructions}) - - for message in get_response_kwargs.get("input", []): - if "role" in message: - messages_by_role[message.get("role")].append( - {"type": "text", "text": message.get("content")} - ) - else: - if message.get("type") == "function_call": - messages_by_role["assistant"].append(message) - elif message.get("type") == "function_call_output": - messages_by_role["tool"].append(message) - - request_messages = [] - for role, messages in messages_by_role.items(): - if len(messages) > 0: - request_messages.append({"role": role, "content": messages}) - - span.set_attribute( - SPANDATA.GEN_AI_REQUEST_MESSAGES, safe_serialize(request_messages) - ) - - -def _set_output_data(span, result): - # type: (sentry_sdk.tracing.Span, Any) -> None - if not should_send_default_pii(): - return - - output_messages = { - "response": [], - "tool": [], - } # type: (dict[str, list[Any]]) - - for output in result.output: - if output.type == "function_call": - output_messages["tool"].append(output.dict()) - elif output.type == "message": - for output_message in output.content: - try: - output_messages["response"].append(output_message.text) - except AttributeError: - # Unknown output message type, just return the json - output_messages["response"].append(output_message.dict()) - - if len(output_messages["tool"]) > 0: - span.set_attribute( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(output_messages["tool"]) - ) - - if len(output_messages["response"]) > 0: - span.set_attribute( - SPANDATA.GEN_AI_RESPONSE_TEXT, safe_serialize(output_messages["response"]) - ) - - -def safe_serialize(data): - # type: (Any) -> str - """Safely serialize to a readable string.""" - - def serialize_item(item): - # type: (Any) -> Union[str, dict[Any, Any], list[Any], tuple[Any, ...]] - if callable(item): - try: - module = getattr(item, "__module__", None) - qualname = getattr(item, "__qualname__", None) - name = getattr(item, "__name__", "anonymous") - - if module and qualname: - full_path = f"{module}.{qualname}" - elif module and name: - full_path = f"{module}.{name}" - else: - full_path = name - - return f"" - except Exception: - return f"" - elif isinstance(item, dict): - return {k: serialize_item(v) for k, v in item.items()} - elif isinstance(item, (list, tuple)): - return [serialize_item(x) for x in item] - elif hasattr(item, "__dict__"): - try: - attrs = { - k: serialize_item(v) - for k, v in vars(item).items() - if not k.startswith("_") - } - return f"<{type(item).__name__} {attrs}>" - except Exception: - return repr(item) - else: - return item - - try: - serialized = serialize_item(data) - return json.dumps(serialized, default=str) - except Exception: - return str(data) diff --git a/tests/integrations/openai_agents/__init__.py b/tests/integrations/openai_agents/__init__.py deleted file mode 100644 index 6940e2bbbe..0000000000 --- a/tests/integrations/openai_agents/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import pytest - -pytest.importorskip("agents") diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py deleted file mode 100644 index ec606c8806..0000000000 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ /dev/null @@ -1,580 +0,0 @@ -import re -import pytest -from unittest.mock import MagicMock, patch -import os - -from sentry_sdk.integrations.openai_agents import OpenAIAgentsIntegration -from sentry_sdk.integrations.openai_agents.utils import safe_serialize - -import agents -from agents import ( - Agent, - ModelResponse, - Usage, - ModelSettings, -) -from agents.items import ( - ResponseOutputMessage, - ResponseOutputText, - ResponseFunctionToolCall, -) - -test_run_config = agents.RunConfig(tracing_disabled=True) - - -@pytest.fixture -def mock_usage(): - return Usage( - requests=1, - input_tokens=10, - output_tokens=20, - total_tokens=30, - input_tokens_details=MagicMock(cached_tokens=0), - output_tokens_details=MagicMock(reasoning_tokens=5), - ) - - -@pytest.fixture -def mock_model_response(mock_usage): - return ModelResponse( - output=[ - ResponseOutputMessage( - id="msg_123", - type="message", - status="completed", - content=[ - ResponseOutputText( - text="Hello, how can I help you?", - type="output_text", - annotations=[], - ) - ], - role="assistant", - ) - ], - usage=mock_usage, - response_id="resp_123", - ) - - -@pytest.fixture -def test_agent(): - """Create a real Agent instance for testing.""" - return Agent( - name="test_agent", - instructions="You are a helpful test assistant.", - model="gpt-4", - model_settings=ModelSettings( - max_tokens=100, - temperature=0.7, - top_p=1.0, - presence_penalty=0.0, - frequency_penalty=0.0, - ), - ) - - -@pytest.mark.asyncio -async def test_agent_invocation_span( - sentry_init, capture_events, test_agent, mock_model_response -): - """ - Test that the integration creates spans for agent invocations. - """ - - with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): - with patch( - "agents.models.openai_responses.OpenAIResponsesModel.get_response" - ) as mock_get_response: - mock_get_response.return_value = mock_model_response - - sentry_init( - integrations=[OpenAIAgentsIntegration()], - traces_sample_rate=1.0, - ) - - events = capture_events() - - result = await agents.Runner.run( - test_agent, "Test input", run_config=test_run_config - ) - - assert result is not None - assert result.final_output == "Hello, how can I help you?" - - (transaction,) = events - spans = transaction["spans"] - invoke_agent_span, ai_client_span = spans - - assert transaction["transaction"] == "test_agent workflow" - assert transaction["contexts"]["trace"]["origin"] == "auto.ai.openai_agents" - - assert invoke_agent_span["description"] == "invoke_agent test_agent" - assert invoke_agent_span["data"]["gen_ai.operation.name"] == "invoke_agent" - assert invoke_agent_span["data"]["gen_ai.system"] == "openai" - assert invoke_agent_span["data"]["gen_ai.agent.name"] == "test_agent" - assert invoke_agent_span["data"]["gen_ai.request.max_tokens"] == 100 - assert invoke_agent_span["data"]["gen_ai.request.model"] == "gpt-4" - assert invoke_agent_span["data"]["gen_ai.request.temperature"] == 0.7 - assert invoke_agent_span["data"]["gen_ai.request.top_p"] == 1.0 - - assert ai_client_span["description"] == "chat gpt-4" - assert ai_client_span["data"]["gen_ai.operation.name"] == "chat" - assert ai_client_span["data"]["gen_ai.system"] == "openai" - assert ai_client_span["data"]["gen_ai.agent.name"] == "test_agent" - assert ai_client_span["data"]["gen_ai.request.max_tokens"] == 100 - assert ai_client_span["data"]["gen_ai.request.model"] == "gpt-4" - assert ai_client_span["data"]["gen_ai.request.temperature"] == 0.7 - assert ai_client_span["data"]["gen_ai.request.top_p"] == 1.0 - - -def test_agent_invocation_span_sync( - sentry_init, capture_events, test_agent, mock_model_response -): - """ - Test that the integration creates spans for agent invocations. - """ - - with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): - with patch( - "agents.models.openai_responses.OpenAIResponsesModel.get_response" - ) as mock_get_response: - mock_get_response.return_value = mock_model_response - - sentry_init( - integrations=[OpenAIAgentsIntegration()], - traces_sample_rate=1.0, - ) - - events = capture_events() - - result = agents.Runner.run_sync( - test_agent, "Test input", run_config=test_run_config - ) - - assert result is not None - assert result.final_output == "Hello, how can I help you?" - - (transaction,) = events - spans = transaction["spans"] - invoke_agent_span, ai_client_span = spans - - assert transaction["transaction"] == "test_agent workflow" - assert transaction["contexts"]["trace"]["origin"] == "auto.ai.openai_agents" - - assert invoke_agent_span["description"] == "invoke_agent test_agent" - assert invoke_agent_span["data"]["gen_ai.operation.name"] == "invoke_agent" - assert invoke_agent_span["data"]["gen_ai.system"] == "openai" - assert invoke_agent_span["data"]["gen_ai.agent.name"] == "test_agent" - assert invoke_agent_span["data"]["gen_ai.request.max_tokens"] == 100 - assert invoke_agent_span["data"]["gen_ai.request.model"] == "gpt-4" - assert invoke_agent_span["data"]["gen_ai.request.temperature"] == 0.7 - assert invoke_agent_span["data"]["gen_ai.request.top_p"] == 1.0 - - assert ai_client_span["description"] == "chat gpt-4" - assert ai_client_span["data"]["gen_ai.operation.name"] == "chat" - assert ai_client_span["data"]["gen_ai.system"] == "openai" - assert ai_client_span["data"]["gen_ai.agent.name"] == "test_agent" - assert ai_client_span["data"]["gen_ai.request.max_tokens"] == 100 - assert ai_client_span["data"]["gen_ai.request.model"] == "gpt-4" - assert ai_client_span["data"]["gen_ai.request.temperature"] == 0.7 - assert ai_client_span["data"]["gen_ai.request.top_p"] == 1.0 - - -@pytest.mark.asyncio -async def test_handoff_span(sentry_init, capture_events, mock_usage): - """ - Test that handoff spans are created when agents hand off to other agents. - """ - # Create two simple agents with a handoff relationship - secondary_agent = agents.Agent( - name="secondary_agent", - instructions="You are a secondary agent.", - model="gpt-4o-mini", - ) - - primary_agent = agents.Agent( - name="primary_agent", - instructions="You are a primary agent that hands off to secondary agent.", - model="gpt-4o-mini", - handoffs=[secondary_agent], - ) - - with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): - with patch( - "agents.models.openai_responses.OpenAIResponsesModel.get_response" - ) as mock_get_response: - # Mock two responses: - # 1. Primary agent calls handoff tool - # 2. Secondary agent provides final response - handoff_response = ModelResponse( - output=[ - ResponseFunctionToolCall( - id="call_handoff_123", - call_id="call_handoff_123", - name="transfer_to_secondary_agent", - type="function_call", - arguments="{}", - function=MagicMock( - name="transfer_to_secondary_agent", arguments="{}" - ), - ) - ], - usage=mock_usage, - response_id="resp_handoff_123", - ) - - final_response = ModelResponse( - output=[ - ResponseOutputMessage( - id="msg_final", - type="message", - status="completed", - content=[ - ResponseOutputText( - text="I'm the specialist and I can help with that!", - type="output_text", - annotations=[], - ) - ], - role="assistant", - ) - ], - usage=mock_usage, - response_id="resp_final_123", - ) - - mock_get_response.side_effect = [handoff_response, final_response] - - sentry_init( - integrations=[OpenAIAgentsIntegration()], - traces_sample_rate=1.0, - ) - - events = capture_events() - - result = await agents.Runner.run( - primary_agent, - "Please hand off to secondary agent", - run_config=test_run_config, - ) - - assert result is not None - - (transaction,) = events - spans = transaction["spans"] - handoff_span = spans[2] - - # Verify handoff span was created - assert handoff_span is not None - assert ( - handoff_span["description"] == "handoff from primary_agent to secondary_agent" - ) - assert handoff_span["data"]["gen_ai.operation.name"] == "handoff" - - -@pytest.mark.asyncio -async def test_tool_execution_span(sentry_init, capture_events, test_agent): - """ - Test tool execution span creation. - """ - - @agents.function_tool - def simple_test_tool(message: str) -> str: - """A simple tool""" - return f"Tool executed with: {message}" - - # Create agent with the tool - agent_with_tool = test_agent.clone(tools=[simple_test_tool]) - - with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): - with patch( - "agents.models.openai_responses.OpenAIResponsesModel.get_response" - ) as mock_get_response: - # Create a mock response that includes tool calls - tool_call = ResponseFunctionToolCall( - id="call_123", - call_id="call_123", - name="simple_test_tool", - type="function_call", - arguments='{"message": "hello"}', - function=MagicMock( - name="simple_test_tool", arguments='{"message": "hello"}' - ), - ) - - # First response with tool call - tool_response = ModelResponse( - output=[tool_call], - usage=Usage( - requests=1, input_tokens=10, output_tokens=5, total_tokens=15 - ), - response_id="resp_tool_123", - ) - - # Second response with final answer - final_response = ModelResponse( - output=[ - ResponseOutputMessage( - id="msg_final", - type="message", - status="completed", - content=[ - ResponseOutputText( - text="Task completed using the tool", - type="output_text", - annotations=[], - ) - ], - role="assistant", - ) - ], - usage=Usage( - requests=1, input_tokens=15, output_tokens=10, total_tokens=25 - ), - response_id="resp_final_123", - ) - - # Return different responses on successive calls - mock_get_response.side_effect = [tool_response, final_response] - - sentry_init( - integrations=[OpenAIAgentsIntegration()], - traces_sample_rate=1.0, - send_default_pii=True, - ) - - events = capture_events() - - await agents.Runner.run( - agent_with_tool, - "Please use the simple test tool", - run_config=test_run_config, - ) - - (transaction,) = events - spans = transaction["spans"] - ( - agent_span, - ai_client_span1, - tool_span, - ai_client_span2, - ) = spans - - available_tools = safe_serialize( - [ - { - "name": "simple_test_tool", - "description": "A simple tool", - "params_json_schema": { - "properties": {"message": {"title": "Message", "type": "string"}}, - "required": ["message"], - "title": "simple_test_tool_args", - "type": "object", - "additionalProperties": False, - }, - "on_invoke_tool": "._create_function_tool.._on_invoke_tool>", - "strict_json_schema": True, - "is_enabled": True, - } - ] - ) - - assert transaction["transaction"] == "test_agent workflow" - assert transaction["contexts"]["trace"]["origin"] == "auto.ai.openai_agents" - - assert agent_span["description"] == "invoke_agent test_agent" - assert agent_span["origin"] == "auto.ai.openai_agents" - assert agent_span["data"]["gen_ai.agent.name"] == "test_agent" - assert agent_span["data"]["gen_ai.operation.name"] == "invoke_agent" - assert agent_span["data"]["gen_ai.request.available_tools"] == available_tools - assert agent_span["data"]["gen_ai.request.max_tokens"] == 100 - assert agent_span["data"]["gen_ai.request.model"] == "gpt-4" - assert agent_span["data"]["gen_ai.request.temperature"] == 0.7 - assert agent_span["data"]["gen_ai.request.top_p"] == 1.0 - assert agent_span["data"]["gen_ai.system"] == "openai" - - assert ai_client_span1["description"] == "chat gpt-4" - assert ai_client_span1["data"]["gen_ai.operation.name"] == "chat" - assert ai_client_span1["data"]["gen_ai.system"] == "openai" - assert ai_client_span1["data"]["gen_ai.agent.name"] == "test_agent" - assert ai_client_span1["data"]["gen_ai.request.available_tools"] == available_tools - assert ai_client_span1["data"]["gen_ai.request.max_tokens"] == 100 - assert ai_client_span1["data"]["gen_ai.request.messages"] == safe_serialize( - [ - { - "role": "system", - "content": [ - {"type": "text", "text": "You are a helpful test assistant."} - ], - }, - { - "role": "user", - "content": [ - {"type": "text", "text": "Please use the simple test tool"} - ], - }, - ] - ) - assert ai_client_span1["data"]["gen_ai.request.model"] == "gpt-4" - assert ai_client_span1["data"]["gen_ai.request.temperature"] == 0.7 - assert ai_client_span1["data"]["gen_ai.request.top_p"] == 1.0 - assert ai_client_span1["data"]["gen_ai.usage.input_tokens"] == 10 - assert ai_client_span1["data"]["gen_ai.usage.input_tokens.cached"] == 0 - assert ai_client_span1["data"]["gen_ai.usage.output_tokens"] == 5 - assert ai_client_span1["data"]["gen_ai.usage.output_tokens.reasoning"] == 0 - assert ai_client_span1["data"]["gen_ai.usage.total_tokens"] == 15 - assert re.sub( - r"SerializationIterator\(.*\)", - "NOT_CHECKED", - ai_client_span1["data"]["gen_ai.response.tool_calls"], - ) == safe_serialize( - [ - { - "arguments": '{"message": "hello"}', - "call_id": "call_123", - "name": "simple_test_tool", - "type": "function_call", - "id": "call_123", - "status": None, - "function": "NOT_CHECKED", - } - ] - ) - - 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 ( - re.sub( - "<.*>(,)", - r"'NOT_CHECKED'\1", - agent_span["data"]["gen_ai.request.available_tools"], - ) - == available_tools - ) - 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" - assert tool_span["data"]["gen_ai.tool.type"] == "function" - - assert ai_client_span2["description"] == "chat gpt-4" - assert ai_client_span2["data"]["gen_ai.agent.name"] == "test_agent" - assert ai_client_span2["data"]["gen_ai.operation.name"] == "chat" - assert ( - re.sub( - "<.*>(,)", - r"'NOT_CHECKED'\1", - agent_span["data"]["gen_ai.request.available_tools"], - ) - == available_tools - ) - assert ai_client_span2["data"]["gen_ai.request.max_tokens"] == 100 - assert re.sub( - r"SerializationIterator\(.*\)", - "NOT_CHECKED", - ai_client_span2["data"]["gen_ai.request.messages"], - ) == safe_serialize( - [ - { - "role": "system", - "content": [ - {"type": "text", "text": "You are a helpful test assistant."} - ], - }, - { - "role": "user", - "content": [ - {"type": "text", "text": "Please use the simple test tool"} - ], - }, - { - "role": "assistant", - "content": [ - { - "arguments": '{"message": "hello"}', - "call_id": "call_123", - "name": "simple_test_tool", - "type": "function_call", - "id": "call_123", - "function": "NOT_CHECKED", - } - ], - }, - { - "role": "tool", - "content": [ - { - "call_id": "call_123", - "output": "Tool executed with: hello", - "type": "function_call_output", - } - ], - }, - ] - ) - assert ai_client_span2["data"]["gen_ai.request.model"] == "gpt-4" - assert ai_client_span2["data"]["gen_ai.request.temperature"] == 0.7 - assert ai_client_span2["data"]["gen_ai.request.top_p"] == 1.0 - assert ai_client_span2["data"]["gen_ai.response.text"] == safe_serialize( - ["Task completed using the tool"] - ) - assert ai_client_span2["data"]["gen_ai.system"] == "openai" - assert ai_client_span2["data"]["gen_ai.usage.input_tokens.cached"] == 0 - assert ai_client_span2["data"]["gen_ai.usage.input_tokens"] == 15 - assert ai_client_span2["data"]["gen_ai.usage.output_tokens.reasoning"] == 0 - assert ai_client_span2["data"]["gen_ai.usage.output_tokens"] == 10 - assert ai_client_span2["data"]["gen_ai.usage.total_tokens"] == 25 - - -@pytest.mark.asyncio -async def test_error_handling(sentry_init, capture_events, test_agent): - """ - Test error handling in agent execution. - """ - - with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): - with patch( - "agents.models.openai_responses.OpenAIResponsesModel.get_response" - ) as mock_get_response: - mock_get_response.side_effect = Exception("Model Error") - - sentry_init( - integrations=[OpenAIAgentsIntegration()], - traces_sample_rate=1.0, - ) - - events = capture_events() - - with pytest.raises(Exception, match="Model Error"): - await agents.Runner.run( - test_agent, "Test input", run_config=test_run_config - ) - - ( - error_event, - transaction, - ) = events - - assert error_event["exception"]["values"][0]["type"] == "Exception" - assert error_event["exception"]["values"][0]["value"] == "Model Error" - assert error_event["exception"]["values"][0]["mechanism"]["type"] == "openai_agents" - - spans = transaction["spans"] - (invoke_agent_span, ai_client_span) = spans - - assert transaction["transaction"] == "test_agent workflow" - assert transaction["contexts"]["trace"]["origin"] == "auto.ai.openai_agents" - - assert invoke_agent_span["description"] == "invoke_agent test_agent" - assert invoke_agent_span["origin"] == "auto.ai.openai_agents" - - assert ai_client_span["description"] == "chat gpt-4" - assert ai_client_span["origin"] == "auto.ai.openai_agents" - assert ai_client_span["tags"]["status"] == "internal_error" diff --git a/tox.ini b/tox.ini index 7b76bbc301..4a080577e8 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-06-24T12:35:34.437673+00:00 +# Last generated: 2025-06-25T13:33:01.471693+00:00 [tox] requires = @@ -141,12 +141,10 @@ envlist = {py3.9,py3.11,py3.12}-cohere-v5.11.4 {py3.9,py3.11,py3.12}-cohere-v5.15.0 - {py3.9,py3.11,py3.12}-openai_agents-v0.0.19 - {py3.8,py3.10,py3.11}-huggingface_hub-v0.22.2 {py3.8,py3.11,py3.12}-huggingface_hub-v0.26.5 {py3.8,py3.12,py3.13}-huggingface_hub-v0.30.2 - {py3.8,py3.12,py3.13}-huggingface_hub-v0.33.0 + {py3.8,py3.12,py3.13}-huggingface_hub-v0.33.1 # ~~~ DBs ~~~ @@ -502,13 +500,10 @@ deps = cohere-v5.11.4: cohere==5.11.4 cohere-v5.15.0: cohere==5.15.0 - openai_agents-v0.0.19: openai-agents==0.0.19 - openai_agents: pytest-asyncio - huggingface_hub-v0.22.2: huggingface_hub==0.22.2 huggingface_hub-v0.26.5: huggingface_hub==0.26.5 huggingface_hub-v0.30.2: huggingface_hub==0.30.2 - huggingface_hub-v0.33.0: huggingface_hub==0.33.0 + huggingface_hub-v0.33.1: huggingface_hub==0.33.1 # ~~~ DBs ~~~ @@ -795,7 +790,6 @@ setenv = litestar: TESTPATH=tests/integrations/litestar loguru: TESTPATH=tests/integrations/loguru openai: TESTPATH=tests/integrations/openai - openai_agents: TESTPATH=tests/integrations/openai_agents openfeature: TESTPATH=tests/integrations/openfeature pure_eval: TESTPATH=tests/integrations/pure_eval pymongo: TESTPATH=tests/integrations/pymongo