Skip to content

Commit f0ad7ac

Browse files
Revert "Remove OpenAI agents from Potel (#4523)"
This reverts commit ca42492.
1 parent cd422af commit f0ad7ac

File tree

22 files changed

+1337
-3
lines changed

22 files changed

+1337
-3
lines changed

.github/workflows/test-integrations-ai.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ jobs:
5959
run: |
6060
set -x # print commands that are executed
6161
./scripts/runtox.sh "py${{ matrix.python-version }}-openai-latest"
62+
- name: Test openai_agents latest
63+
run: |
64+
set -x # print commands that are executed
65+
./scripts/runtox.sh "py${{ matrix.python-version }}-openai_agents-latest"
6266
- name: Test huggingface_hub latest
6367
run: |
6468
set -x # print commands that are executed
@@ -121,6 +125,10 @@ jobs:
121125
run: |
122126
set -x # print commands that are executed
123127
./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openai"
128+
- name: Test openai_agents pinned
129+
run: |
130+
set -x # print commands that are executed
131+
./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openai_agents"
124132
- name: Test huggingface_hub pinned
125133
run: |
126134
set -x # print commands that are executed

scripts/populate_tox/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@
152152
"loguru": {
153153
"package": "loguru",
154154
},
155+
"openai_agents": {
156+
"package": "openai-agents",
157+
"deps": {
158+
"*": ["pytest-asyncio"],
159+
},
160+
},
155161
"openfeature": {
156162
"package": "openfeature-sdk",
157163
},

scripts/populate_tox/tox.jinja

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ setenv =
372372
litestar: TESTPATH=tests/integrations/litestar
373373
loguru: TESTPATH=tests/integrations/loguru
374374
openai: TESTPATH=tests/integrations/openai
375+
openai_agents: TESTPATH=tests/integrations/openai_agents
375376
openfeature: TESTPATH=tests/integrations/openfeature
376377
pure_eval: TESTPATH=tests/integrations/pure_eval
377378
pymongo: TESTPATH=tests/integrations/pymongo

scripts/split_tox_gh_actions/split_tox_gh_actions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"cohere",
6464
"langchain",
6565
"openai",
66+
"openai_agents",
6667
"huggingface_hub",
6768
],
6869
"Cloud": [

sentry_sdk/integrations/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ def iter_default_integrations(
147147
"launchdarkly": (9, 8, 0),
148148
"loguru": (0, 7, 0),
149149
"openai": (1, 0, 0),
150+
"openai_agents": (0, 0, 19),
150151
"openfeature": (0, 7, 1),
151152
"quart": (0, 16, 0),
152153
"ray": (2, 7, 0),
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from sentry_sdk.integrations import DidNotEnable, Integration
2+
3+
from .patches import (
4+
_create_get_model_wrapper,
5+
_create_get_all_tools_wrapper,
6+
_create_run_wrapper,
7+
_patch_agent_run,
8+
)
9+
10+
try:
11+
import agents
12+
13+
except ImportError:
14+
raise DidNotEnable("OpenAI Agents not installed")
15+
16+
17+
def _patch_runner():
18+
# type: () -> None
19+
# Create the root span for one full agent run (including eventual handoffs)
20+
# Note agents.run.DEFAULT_AGENT_RUNNER.run_sync is a wrapper around
21+
# agents.run.DEFAULT_AGENT_RUNNER.run. It does not need to be wrapped separately.
22+
# TODO-anton: Also patch streaming runner: agents.Runner.run_streamed
23+
agents.run.DEFAULT_AGENT_RUNNER.run = _create_run_wrapper(
24+
agents.run.DEFAULT_AGENT_RUNNER.run
25+
)
26+
27+
# Creating the actual spans for each agent run.
28+
_patch_agent_run()
29+
30+
31+
def _patch_model():
32+
# type: () -> None
33+
agents.run.AgentRunner._get_model = classmethod(
34+
_create_get_model_wrapper(agents.run.AgentRunner._get_model),
35+
)
36+
37+
38+
def _patch_tools():
39+
# type: () -> None
40+
agents.run.AgentRunner._get_all_tools = classmethod(
41+
_create_get_all_tools_wrapper(agents.run.AgentRunner._get_all_tools),
42+
)
43+
44+
45+
class OpenAIAgentsIntegration(Integration):
46+
identifier = "openai_agents"
47+
48+
@staticmethod
49+
def setup_once():
50+
# type: () -> None
51+
_patch_tools()
52+
_patch_model()
53+
_patch_runner()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SPAN_ORIGIN = "auto.ai.openai_agents"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .models import _create_get_model_wrapper # noqa: F401
2+
from .tools import _create_get_all_tools_wrapper # noqa: F401
3+
from .runner import _create_run_wrapper # noqa: F401
4+
from .agent_run import _patch_agent_run # noqa: F401
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
from functools import wraps
2+
3+
from sentry_sdk.integrations import DidNotEnable
4+
5+
from ..spans import invoke_agent_span, update_invoke_agent_span, handoff_span
6+
7+
from typing import TYPE_CHECKING
8+
9+
if TYPE_CHECKING:
10+
from typing import Any, Optional
11+
12+
13+
try:
14+
import agents
15+
except ImportError:
16+
raise DidNotEnable("OpenAI Agents not installed")
17+
18+
19+
def _patch_agent_run():
20+
# type: () -> None
21+
"""
22+
Patches AgentRunner methods to create agent invocation spans.
23+
This directly patches the execution flow to track when agents start and stop.
24+
"""
25+
26+
# Store original methods
27+
original_run_single_turn = agents.run.AgentRunner._run_single_turn
28+
original_execute_handoffs = agents._run_impl.RunImpl.execute_handoffs
29+
original_execute_final_output = agents._run_impl.RunImpl.execute_final_output
30+
31+
def _start_invoke_agent_span(context_wrapper, agent):
32+
# type: (agents.RunContextWrapper, agents.Agent) -> None
33+
"""Start an agent invocation span"""
34+
# Store the agent on the context wrapper so we can access it later
35+
context_wrapper._sentry_current_agent = agent
36+
invoke_agent_span(context_wrapper, agent)
37+
38+
def _end_invoke_agent_span(context_wrapper, agent, output=None):
39+
# type: (agents.RunContextWrapper, agents.Agent, Optional[Any]) -> None
40+
"""End the agent invocation span"""
41+
# Clear the stored agent
42+
if hasattr(context_wrapper, "_sentry_current_agent"):
43+
delattr(context_wrapper, "_sentry_current_agent")
44+
45+
update_invoke_agent_span(context_wrapper, agent, output)
46+
47+
def _has_active_agent_span(context_wrapper):
48+
# type: (agents.RunContextWrapper) -> bool
49+
"""Check if there's an active agent span for this context"""
50+
return getattr(context_wrapper, "_sentry_current_agent", None) is not None
51+
52+
def _get_current_agent(context_wrapper):
53+
# type: (agents.RunContextWrapper) -> Optional[agents.Agent]
54+
"""Get the current agent from context wrapper"""
55+
return getattr(context_wrapper, "_sentry_current_agent", None)
56+
57+
@wraps(
58+
original_run_single_turn.__func__
59+
if hasattr(original_run_single_turn, "__func__")
60+
else original_run_single_turn
61+
)
62+
async def patched_run_single_turn(cls, *args, **kwargs):
63+
# type: (agents.Runner, *Any, **Any) -> Any
64+
"""Patched _run_single_turn that creates agent invocation spans"""
65+
66+
agent = kwargs.get("agent")
67+
context_wrapper = kwargs.get("context_wrapper")
68+
should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks")
69+
70+
# Start agent span when agent starts (but only once per agent)
71+
if should_run_agent_start_hooks and agent and context_wrapper:
72+
# End any existing span for a different agent
73+
if _has_active_agent_span(context_wrapper):
74+
current_agent = _get_current_agent(context_wrapper)
75+
if current_agent and current_agent != agent:
76+
_end_invoke_agent_span(context_wrapper, current_agent)
77+
78+
_start_invoke_agent_span(context_wrapper, agent)
79+
80+
# Call original method with all the correct parameters
81+
result = await original_run_single_turn(*args, **kwargs)
82+
83+
return result
84+
85+
@wraps(
86+
original_execute_handoffs.__func__
87+
if hasattr(original_execute_handoffs, "__func__")
88+
else original_execute_handoffs
89+
)
90+
async def patched_execute_handoffs(cls, *args, **kwargs):
91+
# type: (agents.Runner, *Any, **Any) -> Any
92+
"""Patched execute_handoffs that creates handoff spans and ends agent span for handoffs"""
93+
94+
context_wrapper = kwargs.get("context_wrapper")
95+
run_handoffs = kwargs.get("run_handoffs")
96+
agent = kwargs.get("agent")
97+
98+
# Create Sentry handoff span for the first handoff (agents library only processes the first one)
99+
if run_handoffs:
100+
first_handoff = run_handoffs[0]
101+
handoff_agent_name = first_handoff.handoff.agent_name
102+
handoff_span(context_wrapper, agent, handoff_agent_name)
103+
104+
# Call original method with all parameters
105+
try:
106+
result = await original_execute_handoffs(*args, **kwargs)
107+
108+
finally:
109+
# End span for current agent after handoff processing is complete
110+
if agent and context_wrapper and _has_active_agent_span(context_wrapper):
111+
_end_invoke_agent_span(context_wrapper, agent)
112+
113+
return result
114+
115+
@wraps(
116+
original_execute_final_output.__func__
117+
if hasattr(original_execute_final_output, "__func__")
118+
else original_execute_final_output
119+
)
120+
async def patched_execute_final_output(cls, *args, **kwargs):
121+
# type: (agents.Runner, *Any, **Any) -> Any
122+
"""Patched execute_final_output that ends agent span for final outputs"""
123+
124+
agent = kwargs.get("agent")
125+
context_wrapper = kwargs.get("context_wrapper")
126+
final_output = kwargs.get("final_output")
127+
128+
# Call original method with all parameters
129+
try:
130+
result = await original_execute_final_output(*args, **kwargs)
131+
finally:
132+
# End span for current agent after final output processing is complete
133+
if agent and context_wrapper and _has_active_agent_span(context_wrapper):
134+
_end_invoke_agent_span(context_wrapper, agent, final_output)
135+
136+
return result
137+
138+
# Apply patches
139+
agents.run.AgentRunner._run_single_turn = classmethod(patched_run_single_turn)
140+
agents._run_impl.RunImpl.execute_handoffs = classmethod(patched_execute_handoffs)
141+
agents._run_impl.RunImpl.execute_final_output = classmethod(
142+
patched_execute_final_output
143+
)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from functools import wraps
2+
3+
from sentry_sdk.integrations import DidNotEnable
4+
5+
from ..spans import ai_client_span, update_ai_client_span
6+
7+
from typing import TYPE_CHECKING
8+
9+
if TYPE_CHECKING:
10+
from typing import Any, Callable
11+
12+
13+
try:
14+
import agents
15+
except ImportError:
16+
raise DidNotEnable("OpenAI Agents not installed")
17+
18+
19+
def _create_get_model_wrapper(original_get_model):
20+
# type: (Callable[..., Any]) -> Callable[..., Any]
21+
"""
22+
Wraps the agents.Runner._get_model method to wrap the get_response method of the model to create a AI client span.
23+
"""
24+
25+
@wraps(
26+
original_get_model.__func__
27+
if hasattr(original_get_model, "__func__")
28+
else original_get_model
29+
)
30+
def wrapped_get_model(cls, agent, run_config):
31+
# type: (agents.Runner, agents.Agent, agents.RunConfig) -> agents.Model
32+
33+
model = original_get_model(agent, run_config)
34+
original_get_response = model.get_response
35+
36+
@wraps(original_get_response)
37+
async def wrapped_get_response(*args, **kwargs):
38+
# type: (*Any, **Any) -> Any
39+
with ai_client_span(agent, kwargs) as span:
40+
result = await original_get_response(*args, **kwargs)
41+
42+
update_ai_client_span(span, agent, kwargs, result)
43+
44+
return result
45+
46+
model.get_response = wrapped_get_response
47+
48+
return model
49+
50+
return wrapped_get_model

0 commit comments

Comments
 (0)