Skip to content

Commit 67b93f5

Browse files
authored
fix(openai-agents): fix broken traces with agents handoff on run_stream (#3143)
1 parent e22a488 commit 67b93f5

File tree

9 files changed

+3319
-537
lines changed

9 files changed

+3319
-537
lines changed

package-lock.json

Lines changed: 324 additions & 257 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/opentelemetry-instrumentation-openai-agents/opentelemetry/instrumentation/openai_agents/__init__.py

Lines changed: 284 additions & 102 deletions
Large diffs are not rendered by default.

packages/opentelemetry-instrumentation-openai-agents/opentelemetry/instrumentation/openai_agents/utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import dataclasses
2+
import json
13
import os
24
from opentelemetry import context as context_api
35

@@ -17,3 +19,20 @@ def should_send_prompts():
1719
env_setting = os.getenv("TRACELOOP_TRACE_CONTENT", "true")
1820
override = context_api.get_value("override_enable_content_tracing")
1921
return _is_truthy(env_setting) or bool(override)
22+
23+
24+
class JSONEncoder(json.JSONEncoder):
25+
def default(self, o):
26+
if dataclasses.is_dataclass(o):
27+
return dataclasses.asdict(o)
28+
29+
if hasattr(o, "to_json"):
30+
return o.to_json()
31+
32+
if hasattr(o, "json"):
33+
return o.json()
34+
35+
if hasattr(o, "__class__"):
36+
return o.__class__.__name__
37+
38+
return super().default(o)

packages/opentelemetry-instrumentation-openai-agents/tests/cassettes/test_openai_agents/test_recipe_workflow_agent_handoffs_with_function_tools.yaml

Lines changed: 1271 additions & 0 deletions
Large diffs are not rendered by default.

packages/opentelemetry-instrumentation-openai-agents/tests/conftest.py

Lines changed: 141 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from agents import Agent, function_tool, ModelSettings, WebSearchTool
2020
from pydantic import BaseModel
21+
from typing import List, Dict, Union
2122

2223
pytest_plugins = []
2324

@@ -45,6 +46,13 @@ def environment():
4546
@pytest.fixture(autouse=True)
4647
def clear_exporter(exporter):
4748
exporter.clear()
49+
from opentelemetry.instrumentation.openai_agents import (
50+
_root_span_storage,
51+
_instrumented_tools,
52+
)
53+
54+
_root_span_storage.clear()
55+
_instrumented_tools.clear()
4856

4957

5058
@pytest.fixture(scope="session")
@@ -88,9 +96,7 @@ async def get_weather(city: str) -> str:
8896

8997
return Agent(
9098
name="WeatherAgent",
91-
instructions=(
92-
"You get the weather for a city using the get_weather tool."
93-
),
99+
instructions=("You get the weather for a city using the get_weather tool."),
94100
model="gpt-4.1",
95101
tools=[get_weather],
96102
)
@@ -109,10 +115,12 @@ def web_search_tool_agent():
109115
@pytest.fixture(scope="session")
110116
def handoff_agent():
111117

112-
agent_a = Agent(name="AgentA", instructions="Agent A does something.",
113-
model="gpt-4.1")
114-
agent_b = Agent(name="AgentB", instructions="Agent B does something else.",
115-
model="gpt-4.1")
118+
agent_a = Agent(
119+
name="AgentA", instructions="Agent A does something.", model="gpt-4.1"
120+
)
121+
agent_b = Agent(
122+
name="AgentB", instructions="Agent B does something else.", model="gpt-4.1"
123+
)
116124

117125
class HandoffExample(BaseModel):
118126
message: str
@@ -131,11 +139,136 @@ class HandoffExample(BaseModel):
131139
instructions="You decide which agent to handoff to.",
132140
model="gpt-4.1",
133141
handoffs=[agent_a, agent_b],
134-
tools=[handoff_tool_a, handoff_tool_b]
142+
tools=[handoff_tool_a, handoff_tool_b],
135143
)
136144
return triage_agent
137145

138146

147+
@pytest.fixture(scope="session")
148+
def recipe_workflow_agents():
149+
"""Create Main Chat Agent and Recipe Editor Agent with function tools for recipe management."""
150+
151+
class Recipe(BaseModel):
152+
id: str
153+
name: str
154+
ingredients: List[str]
155+
instructions: List[str]
156+
prep_time: str
157+
cook_time: str
158+
servings: int
159+
160+
class SearchResponse(BaseModel):
161+
status: str
162+
message: str
163+
recipes: Union[Dict[str, Recipe], None] = None
164+
recipe_count: Union[int, None] = None
165+
query: Union[str, None] = None
166+
167+
class EditResponse(BaseModel):
168+
status: str
169+
message: str
170+
modified_recipe: Union[Recipe, None] = None
171+
changes_made: Union[List[str], None] = None
172+
original_recipe: Union[Recipe, None] = None
173+
174+
# Mock recipe database
175+
MOCK_RECIPES = {
176+
"spaghetti_carbonara": {
177+
"id": "spaghetti_carbonara",
178+
"name": "Spaghetti Carbonara",
179+
"ingredients": [
180+
"400g spaghetti",
181+
"200g pancetta",
182+
"4 large eggs",
183+
"100g Pecorino Romano cheese",
184+
],
185+
"instructions": [
186+
"Cook spaghetti",
187+
"Dice pancetta",
188+
"Whisk eggs with cheese",
189+
],
190+
"prep_time": "10 minutes",
191+
"cook_time": "15 minutes",
192+
"servings": 4,
193+
}
194+
}
195+
196+
@function_tool
197+
async def search_recipes(query: str = "") -> SearchResponse:
198+
"""Search and browse recipes in the database."""
199+
if "carbonara" in query.lower():
200+
recipe_data = MOCK_RECIPES["spaghetti_carbonara"]
201+
recipes_dict = {"spaghetti_carbonara": Recipe(**recipe_data)}
202+
return SearchResponse(
203+
status="success",
204+
message=f'Found 1 recipes matching "{query}"',
205+
recipes=recipes_dict,
206+
recipe_count=1,
207+
query=query,
208+
)
209+
return SearchResponse(
210+
status="success",
211+
message="No recipes found",
212+
recipes={},
213+
recipe_count=0,
214+
query=query,
215+
)
216+
217+
@function_tool
218+
async def plan_and_apply_recipe_modifications(
219+
recipe: Recipe, modification_request: str
220+
) -> EditResponse:
221+
"""Plan modifications to a recipe based on user request and apply them."""
222+
223+
if (
224+
"vegetarian" in modification_request.lower()
225+
and "carbonara" in recipe.name.lower()
226+
):
227+
modified_recipe = Recipe(
228+
id=recipe.id,
229+
name="Vegetarian Carbonara",
230+
ingredients=[
231+
"400g spaghetti",
232+
"200g mushrooms",
233+
"4 large eggs",
234+
"100g Pecorino Romano cheese",
235+
],
236+
instructions=[
237+
"Cook spaghetti",
238+
"Sauté mushrooms",
239+
"Whisk eggs with cheese",
240+
],
241+
prep_time=recipe.prep_time,
242+
cook_time=recipe.cook_time,
243+
servings=recipe.servings,
244+
)
245+
return EditResponse(
246+
status="success",
247+
message="Successfully modified Spaghetti Carbonara to be vegetarian",
248+
modified_recipe=modified_recipe,
249+
changes_made=["Replaced pancetta with mushrooms"],
250+
original_recipe=recipe,
251+
)
252+
253+
return EditResponse(status="error", message="Could not modify recipe")
254+
255+
recipe_editor_agent = Agent(
256+
name="Recipe Editor Agent",
257+
instructions="You are a recipe editor specialist. Help users search and modify recipes using your tools.",
258+
model="gpt-4o",
259+
tools=[search_recipes, plan_and_apply_recipe_modifications],
260+
)
261+
262+
main_chat_agent = Agent(
263+
name="Main Chat Agent",
264+
instructions="You handle general conversation and route recipe tasks to the recipe editor agent.",
265+
model="gpt-4o",
266+
handoffs=[recipe_editor_agent],
267+
)
268+
269+
return main_chat_agent, recipe_editor_agent
270+
271+
139272
@pytest.fixture(scope="module")
140273
def vcr_config():
141274
return {"filter_headers": ["authorization", "api-key"]}

0 commit comments

Comments
 (0)