Skip to content

Commit dba82b4

Browse files
committed
Add better tracing for sync_provider
1 parent 7046381 commit dba82b4

File tree

4 files changed

+148
-63
lines changed

4 files changed

+148
-63
lines changed

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,23 @@ dependencies = [
2424
"pyyaml>=6.0.2,<7",
2525
"jsonschema>=4.23.0,<5",
2626
"jsonref>=1.1.0,<2",
27-
"temporalio>=1.10.0,<2",
27+
"temporalio>=1.18.2,<2",
2828
"aiohttp>=3.10.10,<4",
2929
"redis>=5.2.0,<6",
3030
"litellm>=1.66.0,<2",
3131
"kubernetes>=25.0.0,<29.0.0",
3232
"jinja2>=3.1.3,<4",
3333
"mcp[cli]>=1.4.1",
3434
"scale-gp>=0.1.0a59",
35-
"openai-agents==0.2.7", # 0.2.3 bug - https://github.com/openai/openai-agents-python/issues/1276
35+
"openai-agents==0.4.2",
3636
"tzlocal>=5.3.1",
3737
"tzdata>=2025.2",
3838
"pytest>=8.4.0",
3939
"json_log_formatter>=1.1.1",
4040
"pytest-asyncio>=1.0.0",
4141
"scale-gp-beta==0.1.0a20",
4242
"ipykernel>=6.29.5",
43-
"openai==1.99.9", # anything higher than 1.99.9 breaks litellm - https://github.com/BerriAI/litellm/issues/13711
43+
"openai>=2.2,<3", # Required by openai-agents 0.4.2; litellm now supports openai 2.x (issue #13711 resolved: https://github.com/BerriAI/litellm/issues/13711)
4444
"cloudpickle>=3.1.1",
4545
"datadog>=0.52.1",
4646
"ddtrace>=3.13.0"

requirements-dev.lock

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,9 @@ httpx==0.27.2
113113
# via mcp
114114
# via openai
115115
# via respx
116-
httpx-aiohttp==0.1.9
117116
# via scale-gp
118117
# via scale-gp-beta
118+
httpx-aiohttp==0.1.9
119119
# via agentex-sdk
120120
httpx-sse==0.4.1
121121
# via mcp
@@ -191,11 +191,11 @@ nox==2023.4.22
191191
oauthlib==3.3.1
192192
# via kubernetes
193193
# via requests-oauthlib
194-
openai==1.99.9
194+
openai==2.7.1
195195
# via agentex-sdk
196196
# via litellm
197197
# via openai-agents
198-
openai-agents==0.2.7
198+
openai-agents==0.4.2
199199
# via agentex-sdk
200200
opentelemetry-api==1.37.0
201201
# via ddtrace
@@ -219,20 +219,6 @@ prompt-toolkit==3.0.51
219219
propcache==0.3.1
220220
# via aiohttp
221221
# via yarl
222-
pydantic==2.11.9
223-
# via agentex-sdk
224-
# via agentex-sdk
225-
# via fastapi
226-
# via litellm
227-
# via mcp
228-
# via openai
229-
# via openai-agents
230-
# via pydantic-settings
231-
# via python-on-whales
232-
# via scale-gp
233-
# via scale-gp-beta
234-
pydantic-core==2.33.2
235-
# via pydantic
236222
protobuf==5.29.5
237223
# via ddtrace
238224
# via temporalio
@@ -247,6 +233,19 @@ pyasn1==0.6.1
247233
# via rsa
248234
pyasn1-modules==0.4.2
249235
# via google-auth
236+
pydantic==2.11.9
237+
# via agentex-sdk
238+
# via fastapi
239+
# via litellm
240+
# via mcp
241+
# via openai
242+
# via openai-agents
243+
# via pydantic-settings
244+
# via python-on-whales
245+
# via scale-gp
246+
# via scale-gp-beta
247+
pydantic-core==2.33.2
248+
# via pydantic
250249
pydantic-settings==2.10.1
251250
# via mcp
252251
pygments==2.18.0
@@ -340,7 +339,7 @@ stack-data==0.6.3
340339
starlette==0.46.2
341340
# via fastapi
342341
# via mcp
343-
temporalio==1.15.0
342+
temporalio==1.18.2
344343
# via agentex-sdk
345344
tiktoken==0.11.0
346345
# via litellm
@@ -383,16 +382,15 @@ typing-extensions==4.12.2
383382
# via pydantic
384383
# via pydantic-core
385384
# via pyright
386-
# via typing-inspection
387-
typing-inspection==0.4.1
388-
# via pydantic
389385
# via python-on-whales
390386
# via referencing
391387
# via scale-gp
392388
# via scale-gp-beta
393389
# via temporalio
394390
# via typer
395391
# via typing-inspection
392+
typing-inspection==0.4.1
393+
# via pydantic
396394
# via pydantic-settings
397395
tzdata==2025.2
398396
# via agentex-sdk

requirements.lock

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,12 @@ httpcore==1.0.9
9999
httpx==0.27.2
100100
# via agentex-sdk
101101
# via httpx-aiohttp
102-
httpx-aiohttp==0.1.9
103102
# via litellm
104103
# via mcp
105104
# via openai
106105
# via scale-gp
107106
# via scale-gp-beta
107+
httpx-aiohttp==0.1.9
108108
# via agentex-sdk
109109
httpx-sse==0.4.1
110110
# via mcp
@@ -174,11 +174,11 @@ nexus-rpc==1.1.0
174174
oauthlib==3.3.1
175175
# via kubernetes
176176
# via requests-oauthlib
177-
openai==1.99.9
177+
openai==2.7.1
178178
# via agentex-sdk
179179
# via litellm
180180
# via openai-agents
181-
openai-agents==0.2.7
181+
openai-agents==0.4.2
182182
# via agentex-sdk
183183
opentelemetry-api==1.37.0
184184
# via ddtrace
@@ -200,19 +200,6 @@ prompt-toolkit==3.0.51
200200
propcache==0.3.1
201201
# via aiohttp
202202
# via yarl
203-
pydantic==2.11.9
204-
# via agentex-sdk
205-
# via fastapi
206-
# via litellm
207-
# via mcp
208-
# via openai
209-
# via openai-agents
210-
# via pydantic-settings
211-
# via python-on-whales
212-
# via scale-gp
213-
# via scale-gp-beta
214-
pydantic-core==2.33.2
215-
# via pydantic
216203
protobuf==5.29.5
217204
# via ddtrace
218205
# via temporalio
@@ -227,6 +214,19 @@ pyasn1==0.6.1
227214
# via rsa
228215
pyasn1-modules==0.4.2
229216
# via google-auth
217+
pydantic==2.11.9
218+
# via agentex-sdk
219+
# via fastapi
220+
# via litellm
221+
# via mcp
222+
# via openai
223+
# via openai-agents
224+
# via pydantic-settings
225+
# via python-on-whales
226+
# via scale-gp
227+
# via scale-gp-beta
228+
pydantic-core==2.33.2
229+
# via pydantic
230230
pydantic-settings==2.10.1
231231
# via mcp
232232
pygments==2.19.2
@@ -311,7 +311,7 @@ stack-data==0.6.3
311311
starlette==0.46.2
312312
# via fastapi
313313
# via mcp
314-
temporalio==1.15.0
314+
temporalio==1.18.2
315315
# via agentex-sdk
316316
tiktoken==0.11.0
317317
# via litellm
@@ -351,16 +351,15 @@ typing-extensions==4.12.2
351351
# via opentelemetry-api
352352
# via pydantic
353353
# via pydantic-core
354-
# via typing-inspection
355-
typing-inspection==0.4.1
356-
# via pydantic
357354
# via python-on-whales
358355
# via referencing
359356
# via scale-gp
360357
# via scale-gp-beta
361358
# via temporalio
362359
# via typer
363360
# via typing-inspection
361+
typing-inspection==0.4.1
362+
# via pydantic
364363
# via pydantic-settings
365364
tzdata==2025.2
366365
# via agentex-sdk

src/agentex/lib/adk/providers/_modules/sync_provider.py

Lines changed: 107 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from agents.models.openai_provider import OpenAIProvider
2929

3030
from agentex import AsyncAgentex
31+
from agentex.lib.utils.logging import make_logger
3132
from agentex.lib.core.tracing.tracer import AsyncTracer
3233
from agentex.types.task_message_delta import TextDelta
3334
from agentex.types.task_message_update import (
@@ -40,6 +41,44 @@
4041
from agentex.types.tool_request_content import ToolRequestContent
4142
from agentex.types.tool_response_content import ToolResponseContent
4243

44+
logger = make_logger(__name__)
45+
46+
47+
def _serialize_item(item: Any) -> dict[str, Any]:
48+
"""
49+
Universal serializer for any item type from OpenAI Agents SDK.
50+
51+
Uses model_dump() for Pydantic models, otherwise extracts attributes manually.
52+
Filters out internal Pydantic fields that can't be serialized.
53+
"""
54+
if hasattr(item, 'model_dump'):
55+
# Pydantic model - use model_dump for proper serialization
56+
try:
57+
return item.model_dump(mode='json', exclude_unset=True)
58+
except Exception:
59+
# Fallback to dict conversion
60+
return dict(item) if hasattr(item, '__iter__') else {}
61+
else:
62+
# Not a Pydantic model - extract attributes manually
63+
item_dict = {}
64+
for attr_name in dir(item):
65+
if not attr_name.startswith('_') and attr_name not in ('model_fields', 'model_config', 'model_computed_fields'):
66+
try:
67+
attr_value = getattr(item, attr_name, None)
68+
# Skip methods and None values
69+
if attr_value is not None and not callable(attr_value):
70+
# Convert to JSON-serializable format
71+
if hasattr(attr_value, 'model_dump'):
72+
item_dict[attr_name] = attr_value.model_dump()
73+
elif isinstance(attr_value, (str, int, float, bool, list, dict)):
74+
item_dict[attr_name] = attr_value
75+
else:
76+
item_dict[attr_name] = str(attr_value)
77+
except Exception:
78+
# Skip attributes that can't be accessed
79+
pass
80+
return item_dict
81+
4382

4483
class SyncStreamingModel(Model):
4584
"""Simple model wrapper that adds logging to stream_response and supports tracing."""
@@ -109,10 +148,38 @@ async def get_response(
109148

110149
response = await self.original_model.get_response(**kwargs)
111150

112-
# Set span output
113-
if span:
151+
# Set span output with structured data
152+
if span and response:
153+
new_items = []
154+
final_output = None
155+
156+
# Extract final output text from response
157+
response_final_output = getattr(response, 'final_output', None)
158+
if response_final_output:
159+
final_output = response_final_output
160+
161+
# Extract items from the response output
162+
response_output = getattr(response, 'output', None)
163+
if response_output:
164+
output_items = response_output if isinstance(response_output, list) else [response_output]
165+
166+
for item in output_items:
167+
item_dict = _serialize_item(item)
168+
if item_dict:
169+
new_items.append(item_dict)
170+
171+
# Extract final_output from message type if available
172+
if item_dict.get('type') == 'message' and not final_output:
173+
content = item_dict.get('content', [])
174+
if content and isinstance(content, list):
175+
for content_part in content:
176+
if isinstance(content_part, dict) and 'text' in content_part:
177+
final_output = content_part['text']
178+
break
179+
114180
span.output = {
115-
"response": str(response) if response else None,
181+
"new_items": new_items,
182+
"final_output": final_output,
116183
}
117184

118185
return response
@@ -160,7 +227,9 @@ async def stream_response(
160227
# Wrap the streaming in a tracing span if tracer is available
161228
if self.tracer and self.trace_id:
162229
trace = self.tracer.trace(self.trace_id)
163-
async with trace.span(
230+
231+
# Manually start the span instead of using context manager
232+
span = await trace.start_span(
164233
parent_id=self.parent_span_id,
165234
name="run_agent_streamed",
166235
input={
@@ -172,7 +241,9 @@ async def stream_response(
172241
"handoffs": [str(h) for h in handoffs] if handoffs else [],
173242
"previous_response_id": previous_response_id,
174243
},
175-
) as span:
244+
)
245+
246+
try:
176247
# Get the stream from the original model
177248
stream_kwargs = {
178249
"system_instructions": system_instructions,
@@ -193,23 +264,40 @@ async def stream_response(
193264
# Get the stream response from the original model and yield each event
194265
stream_response = self.original_model.stream_response(**stream_kwargs)
195266

196-
# Pass through each event from the original stream
197-
event_count = 0
198-
final_output = None
267+
# Pass through each event from the original stream and track items
268+
new_items = []
269+
final_response_text = ""
270+
199271
async for event in stream_response:
200-
event_count += 1
201-
# Track the final output if available
202-
if hasattr(event, 'type') and event.type == 'raw_response_event':
203-
if hasattr(event.data, 'output'):
204-
final_output = event.data.output
272+
event_type = getattr(event, 'type', 'no-type')
273+
274+
# Handle response.output_item.done events which contain completed items
275+
if event_type == 'response.output_item.done':
276+
item = getattr(event, 'item', None)
277+
if item is not None:
278+
item_dict = _serialize_item(item)
279+
if item_dict:
280+
new_items.append(item_dict)
281+
282+
# Update final_response_text from message type if available
283+
if item_dict.get('type') == 'message':
284+
content = item_dict.get('content', [])
285+
if content and isinstance(content, list):
286+
for content_part in content:
287+
if isinstance(content_part, dict) and 'text' in content_part:
288+
final_response_text = content_part['text']
289+
break
290+
205291
yield event
206292

207-
# Set span output
208-
if span:
209-
span.output = {
210-
"event_count": event_count,
211-
"final_output": str(final_output) if final_output else None,
212-
}
293+
# Set span output with structured data including tool calls and final response
294+
span.output = {
295+
"new_items": new_items,
296+
"final_output": final_response_text if final_response_text else None,
297+
}
298+
finally:
299+
# End the span after all events have been yielded
300+
await trace.end_span(span)
213301
else:
214302
# No tracing, just stream normally
215303
# Get the stream from the original model

0 commit comments

Comments
 (0)