Skip to content

Commit 233e3b9

Browse files
committed
test: fix sys.modules stub leakage and apply repo formatting
test_claude_agents_* now remove their placeholder packages after loading, which previously blocked real imports of the temporal plugin tree in later-collected tests. Formats touched files with ruff and narrows span.output types in new tests for pyright.
1 parent 503f940 commit 233e3b9

8 files changed

Lines changed: 120 additions & 100 deletions

File tree

src/agentex/lib/adk/_modules/tracing.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,7 @@ async def turn_span(
247247
248248
Example::
249249
250-
async with adk.tracing.turn_span(
251-
trace_id=task.id, name="turn", input={...}, task_id=task.id
252-
) as turn:
250+
async with adk.tracing.turn_span(trace_id=task.id, name="turn", input={...}, task_id=task.id) as turn:
253251
result = await run_llm_calls()
254252
turn.output = {"response": result.text}
255253
turn.record_usage(usage=result.usage, cost_usd=result.cost_usd)

src/agentex/lib/core/temporal/plugins/openai_agents/models/temporal_tracing_model.py

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
The key innovation is that these are thin wrappers around the standard OpenAI models,
88
avoiding code duplication while adding tracing capabilities.
99
"""
10+
1011
from __future__ import annotations
1112

1213
import logging
@@ -51,24 +52,28 @@ def _serialize_item(item: Any) -> dict[str, Any]:
5152
Uses model_dump() for Pydantic models, otherwise extracts attributes manually.
5253
Filters out internal Pydantic fields that can't be serialized.
5354
"""
54-
if hasattr(item, 'model_dump'):
55+
if hasattr(item, "model_dump"):
5556
# Pydantic model - use model_dump for proper serialization
5657
try:
57-
return item.model_dump(mode='json', exclude_unset=True)
58+
return item.model_dump(mode="json", exclude_unset=True)
5859
except Exception:
5960
# Fallback to dict conversion
60-
return dict(item) if hasattr(item, '__iter__') else {}
61+
return dict(item) if hasattr(item, "__iter__") else {}
6162
else:
6263
# Not a Pydantic model - extract attributes manually
6364
item_dict = {}
6465
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+
if not attr_name.startswith("_") and attr_name not in (
67+
"model_fields",
68+
"model_config",
69+
"model_computed_fields",
70+
):
6671
try:
6772
attr_value = getattr(item, attr_name, None)
6873
# Skip methods and None values
6974
if attr_value is not None and not callable(attr_value):
7075
# Convert to JSON-serializable format
71-
if hasattr(attr_value, 'model_dump'):
76+
if hasattr(attr_value, "model_dump"):
7277
item_dict[attr_name] = attr_value.model_dump()
7378
elif isinstance(attr_value, (str, int, float, bool, list, dict)):
7479
item_dict[attr_name] = attr_value
@@ -106,7 +111,9 @@ def __init__(self, openai_client: Optional[AsyncOpenAI] = None, **kwargs):
106111
# Initialize tracer for all models
107112
agentex_client = create_async_agentex_client()
108113
self._tracer = AsyncTracer(agentex_client)
109-
logger.info(f"[TemporalTracingModelProvider] Initialized with AgentEx tracer, custom_client={openai_client is not None}")
114+
logger.info(
115+
f"[TemporalTracingModelProvider] Initialized with AgentEx tracer, custom_client={openai_client is not None}"
116+
)
110117

111118
@override
112119
def get_model(self, model_name: Optional[str]) -> Model:
@@ -126,10 +133,14 @@ def get_model(self, model_name: Optional[str]) -> Model:
126133
logger.info(f"[TemporalTracingModelProvider] Wrapping OpenAIResponsesModel '{model_name}' with tracing")
127134
return TemporalTracingResponsesModel(base_model, self._tracer) # type: ignore[abstract]
128135
elif isinstance(base_model, OpenAIChatCompletionsModel):
129-
logger.info(f"[TemporalTracingModelProvider] Wrapping OpenAIChatCompletionsModel '{model_name}' with tracing")
136+
logger.info(
137+
f"[TemporalTracingModelProvider] Wrapping OpenAIChatCompletionsModel '{model_name}' with tracing"
138+
)
130139
return TemporalTracingChatCompletionsModel(base_model, self._tracer) # type: ignore[abstract]
131140
else:
132-
logger.warning(f"[TemporalTracingModelProvider] Unknown model type, returning without tracing: {type(base_model)}")
141+
logger.warning(
142+
f"[TemporalTracingModelProvider] Unknown model type, returning without tracing: {type(base_model)}"
143+
)
133144
return base_model
134145

135146

@@ -181,7 +192,9 @@ async def get_response(
181192

182193
# If we have tracing context, wrap with span
183194
if trace_id and parent_span_id:
184-
logger.debug(f"[TemporalTracingResponsesModel] Adding tracing span for task_id={task_id}, trace_id={trace_id}")
195+
logger.debug(
196+
f"[TemporalTracingResponsesModel] Adding tracing span for task_id={task_id}, trace_id={trace_id}"
197+
)
185198

186199
trace = self._tracer.trace(trace_id)
187200

@@ -199,7 +212,9 @@ async def get_response(
199212
"temperature": model_settings.temperature,
200213
"max_tokens": model_settings.max_tokens,
201214
"reasoning": model_settings.reasoning,
202-
} if model_settings else None,
215+
}
216+
if model_settings
217+
else None,
203218
},
204219
) as span:
205220
try:
@@ -222,7 +237,7 @@ async def get_response(
222237
new_items = []
223238
final_output = None
224239

225-
if hasattr(response, 'output') and response.output:
240+
if hasattr(response, "output") and response.output:
226241
response_output = response.output if isinstance(response.output, list) else [response.output]
227242

228243
for item in response_output:
@@ -232,12 +247,12 @@ async def get_response(
232247
new_items.append(item_dict)
233248

234249
# Extract final_output from message type if available
235-
if item_dict.get('type') == 'message' and not final_output:
236-
content = item_dict.get('content', [])
250+
if item_dict.get("type") == "message" and not final_output:
251+
content = item_dict.get("content", [])
237252
if content and isinstance(content, list):
238253
for content_part in content:
239-
if isinstance(content_part, dict) and 'text' in content_part:
240-
final_output = content_part['text']
254+
if isinstance(content_part, dict) and "text" in content_part:
255+
final_output = content_part["text"]
241256
break
242257
except Exception as e:
243258
logger.warning(f"Failed to serialize item in temporal tracing model: {e}")
@@ -329,7 +344,9 @@ async def get_response(
329344

330345
# If we have tracing context, wrap with span
331346
if trace_id and parent_span_id:
332-
logger.debug(f"[TemporalTracingChatCompletionsModel] Adding tracing span for task_id={task_id}, trace_id={trace_id}")
347+
logger.debug(
348+
f"[TemporalTracingChatCompletionsModel] Adding tracing span for task_id={task_id}, trace_id={trace_id}"
349+
)
333350

334351
trace = self._tracer.trace(trace_id)
335352

@@ -346,7 +363,9 @@ async def get_response(
346363
"model_settings": {
347364
"temperature": model_settings.temperature,
348365
"max_tokens": model_settings.max_tokens,
349-
} if model_settings else None,
366+
}
367+
if model_settings
368+
else None,
350369
},
351370
) as span:
352371
try:
@@ -366,7 +385,7 @@ async def get_response(
366385
new_items = []
367386
final_output = None
368387

369-
if hasattr(response, 'output') and response.output:
388+
if hasattr(response, "output") and response.output:
370389
response_output = response.output if isinstance(response.output, list) else [response.output]
371390

372391
for item in response_output:
@@ -376,12 +395,12 @@ async def get_response(
376395
new_items.append(item_dict)
377396

378397
# Extract final_output from message type if available
379-
if item_dict.get('type') == 'message' and not final_output:
380-
content = item_dict.get('content', [])
398+
if item_dict.get("type") == "message" and not final_output:
399+
content = item_dict.get("content", [])
381400
if content and isinstance(content, list):
382401
for content_part in content:
383-
if isinstance(content_part, dict) and 'text' in content_part:
384-
final_output = content_part['text']
402+
if isinstance(content_part, dict) and "text" in content_part:
403+
final_output = content_part["text"]
385404
break
386405
except Exception as e:
387406
logger.warning(f"Failed to serialize item in temporal tracing model: {e}")
@@ -406,7 +425,9 @@ async def get_response(
406425
raise
407426
else:
408427
# No tracing context, just pass through
409-
logger.debug("[TemporalTracingChatCompletionsModel] No tracing context available, calling base model directly")
428+
logger.debug(
429+
"[TemporalTracingChatCompletionsModel] No tracing context available, calling base model directly"
430+
)
410431
return await self._base_model.get_response(
411432
system_instructions=system_instructions,
412433
input=input,
@@ -422,4 +443,4 @@ async def get_response(
422443
def stream_response(self, *args, **kwargs):
423444
"""Streaming is handled via get_response in Temporal activities.
424445
Required so the class is concrete and instantiable at runtime."""
425-
raise NotImplementedError("stream_response is not used in Temporal activities - use get_response instead")
446+
raise NotImplementedError("stream_response is not used in Temporal activities - use get_response instead")

src/agentex/lib/utils/completions.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ def _(a: Completion, b: Completion) -> Completion:
2626
# final chunk carries usage but no choices. Keep the unpaired side instead
2727
# of truncating to the shorter list.
2828
a.choices = [
29-
x if y is None else y if x is None else _concat_chunks(x, y)
30-
for x, y in zip_longest(a.choices, b.choices)
29+
x if y is None else y if x is None else _concat_chunks(x, y) for x, y in zip_longest(a.choices, b.choices)
3130
]
3231
a.usage = _concat_chunks(a.usage, b.usage)
3332

@@ -45,6 +44,7 @@ def _(a: Choice, b: Choice) -> Choice:
4544
a.finish_reason = a.finish_reason or b.finish_reason
4645
return a
4746

47+
4848
@_concat_chunks.register
4949
def _(a: Usage | None, b: Usage | None) -> Usage | None:
5050
if a is not None and b is not None:
@@ -68,9 +68,7 @@ def _(a: Delta, b: Delta) -> Delta:
6868
if tool_call.index not in grouped_tool_calls:
6969
grouped_tool_calls[tool_call.index] = tool_call
7070
else:
71-
grouped_tool_calls[tool_call.index] = _concat_chunks(
72-
grouped_tool_calls[tool_call.index], tool_call
73-
)
71+
grouped_tool_calls[tool_call.index] = _concat_chunks(grouped_tool_calls[tool_call.index], tool_call)
7472

7573
a.tool_calls = list(grouped_tool_calls.values())
7674
elif hasattr(b, "tool_calls") and b.tool_calls:
@@ -88,11 +86,7 @@ def _(a: ToolCallRequest, b: ToolCallRequest) -> ToolCallRequest:
8886
index_val = a.index if hasattr(a, "index") and a.index is not None else b.index
8987

9088
# Concatenate the function part
91-
function_val = (
92-
_concat_chunks(a.function, b.function)
93-
if a.function and b.function
94-
else a.function or b.function
95-
)
89+
function_val = _concat_chunks(a.function, b.function) if a.function and b.function else a.function or b.function
9690

9791
# Set all properties
9892
a.id = id_val

tests/lib/adk/providers/test_litellm_usage.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77

88
from __future__ import annotations
99

10+
from typing import Any
1011
from datetime import UTC, datetime
1112
from contextlib import asynccontextmanager
1213
from unittest.mock import AsyncMock, MagicMock
1314

1415
from agentex.types.span import Span
1516
from agentex.types.task_message import TaskMessage
16-
from agentex.types.task_message_content import TextContent
1717
from agentex.lib.types.llm_messages import (
1818
Delta,
1919
Usage,
@@ -22,6 +22,7 @@
2222
Completion,
2323
AssistantMessage,
2424
)
25+
from agentex.types.task_message_content import TextContent
2526
from agentex.lib.core.services.adk.providers.litellm import (
2627
LiteLLMService,
2728
_stream_kwargs_with_usage,
@@ -54,6 +55,11 @@ def trace(self, trace_id):
5455
return self.trace_obj
5556

5657

58+
def _output_dict(span: Span) -> dict[str, Any]:
59+
assert isinstance(span.output, dict)
60+
return span.output
61+
62+
5763
def _make_streaming_service():
5864
streaming_context = MagicMock()
5965
streaming_context.task_message = TaskMessage(
@@ -142,7 +148,7 @@ async def test_span_output_carries_usage(self):
142148
)
143149

144150
span = tracer.trace_obj.spans[0]
145-
assert span.output["usage"] == {"prompt_tokens": 7, "completion_tokens": 3, "total_tokens": 10}
151+
assert _output_dict(span)["usage"] == {"prompt_tokens": 7, "completion_tokens": 3, "total_tokens": 10}
146152

147153
async def test_span_output_omits_usage_when_absent(self):
148154
completion = Completion(
@@ -158,7 +164,7 @@ async def test_span_output_omits_usage_when_absent(self):
158164
trace_id="trace-1",
159165
)
160166

161-
assert "usage" not in tracer.trace_obj.spans[0].output
167+
assert "usage" not in _output_dict(tracer.trace_obj.spans[0])
162168

163169

164170
class TestChatCompletionStream:
@@ -177,8 +183,8 @@ async def test_stream_requests_usage_and_span_output_carries_it(self):
177183
assert len(results) == 3
178184
assert captured_kwargs["stream_options"] == {"include_usage": True}
179185
span = tracer.trace_obj.spans[0]
180-
assert span.output["usage"] == {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
181-
assert span.output["choices"][0]["message"]["content"] == "Hello!"
186+
assert _output_dict(span)["usage"] == {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
187+
assert _output_dict(span)["choices"][0]["message"]["content"] == "Hello!"
182188

183189

184190
class TestChatCompletionStreamAutoSend:
@@ -195,9 +201,9 @@ async def test_usage_only_final_chunk_reaches_span_output(self):
195201

196202
assert captured_kwargs["stream_options"] == {"include_usage": True}
197203
span = tracer.trace_obj.spans[0]
198-
assert span.output["usage"] == {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
204+
assert _output_dict(span)["usage"] == {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
199205
# TaskMessage dump is still the base of the span output
200-
assert span.output["id"] == "msg-1"
206+
assert _output_dict(span)["id"] == "msg-1"
201207

202208
async def test_stream_without_usage_chunk_omits_usage(self):
203209
captured_kwargs: dict = {}
@@ -210,4 +216,4 @@ async def test_stream_without_usage_chunk_omits_usage(self):
210216
trace_id="trace-1",
211217
)
212218

213-
assert "usage" not in tracer.trace_obj.spans[0].output
219+
assert "usage" not in _output_dict(tracer.trace_obj.spans[0])

tests/lib/adk/test_tracing_module.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,9 +314,7 @@ async def test_turn_span_preserves_existing_data(self):
314314
mock_service.end_span.return_value = started
315315

316316
with patch.object(_tracing_mod, "in_temporal_workflow", return_value=False):
317-
async with module.turn_span(
318-
trace_id="trace-123", name="turn", data={"custom": "value"}
319-
) as turn:
317+
async with module.turn_span(trace_id="trace-123", name="turn", data={"custom": "value"}) as turn:
320318
turn.record_usage(usage={"prompt_tokens": 3, "completion_tokens": 4})
321319

322320
ended_span = mock_service.end_span.call_args.kwargs["span"]

0 commit comments

Comments
 (0)