Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions agentops/helpers/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,24 +87,41 @@ def model_to_dict(obj: Any) -> dict:
Returns:
Dictionary representation of the object, or empty dict if conversion fails
"""
if obj is None:
return {}
if isinstance(obj, dict):
return obj
if hasattr(obj, "model_dump"): # Pydantic v2
return obj.model_dump()
elif hasattr(obj, "dict"): # Pydantic v1
return obj.dict()
# TODO this is causing recursion on nested objects.
# elif hasattr(obj, "parse"): # Raw API response
# return model_to_dict(obj.parse())
else:
def _object_dict(value: Any) -> dict:
# Try to use __dict__ as fallback
try:
return obj.__dict__
return value.__dict__
except:
return {}

def _model_to_dict(value: Any, seen: set[int]) -> dict:
if value is None:
return {}
if isinstance(value, dict):
return value

value_id = id(value)
if value_id in seen:
return {}
seen.add(value_id)

if hasattr(value, "model_dump"): # Pydantic v2
return value.model_dump()
elif hasattr(value, "dict"): # Pydantic v1
return value.dict()
elif hasattr(value, "parse"): # Raw API response
try:
parsed = value.parse()
except Exception:
return _object_dict(value)
if parsed is value:
return _object_dict(value)
return _model_to_dict(parsed, seen)
else:
return _object_dict(value)

return _model_to_dict(obj, set())


def safe_serialize(obj: Any) -> Any:
"""Safely serialize an object to JSON-compatible format
Expand Down
28 changes: 25 additions & 3 deletions tests/unit/test_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,10 @@ def test_pydantic_models(self):
v2_result = safe_serialize(v2_model)
assert json.loads(v2_result) == {"name": "test", "value": 42}

# Note: parse() method is currently not implemented due to recursion issues
# See TODO in serialization.py
# Model with parse() method
parse_model = ModelWithParse({"name": "test", "value": 42})
parse_result = safe_serialize(parse_model)
assert json.loads(parse_result) == {"name": "test", "value": 42}

def test_special_types(self):
"""Test serialization of special types using AgentOpsJSONEncoder."""
Expand Down Expand Up @@ -406,12 +408,32 @@ def test_pydantic_models(self):
v2_model = PydanticV2Model(name="test", value=42)
assert model_to_dict(v2_model) == {"name": "test", "value": 42}

@pytest.mark.skip(reason="parse() method handling is currently commented out in the implementation")
def test_parse_method(self):
"""Test models with parse method."""
parse_model = ModelWithParse({"name": "test", "value": 42})
assert model_to_dict(parse_model) == {"name": "test", "value": 42}

def test_parse_method_self_reference(self):
"""Test parse methods that return themselves do not recurse indefinitely."""

class SelfParsingModel:
def parse(self):
return self

assert model_to_dict(SelfParsingModel()) == {}

def test_parse_method_exception_uses_dict_fallback(self):
"""Test parse failures still use the existing __dict__ fallback."""

class FailingParseModel:
def __init__(self):
self.value = "fallback"

def parse(self):
raise ValueError("parse failed")

assert model_to_dict(FailingParseModel()) == {"value": "fallback"}

def test_dict_fallback(self):
"""Test fallback to __dict__."""
simple_model = SimpleModel("test value")
Expand Down