From 6d45ceb623891810b2b8c7b47943169934b4b436 Mon Sep 17 00:00:00 2001 From: pragnyanramtha Date: Sun, 17 May 2026 01:37:34 +0000 Subject: [PATCH] Restore parse model serialization --- agentops/helpers/serialization.py | 43 +++++++++++++++++++++---------- tests/unit/test_serialization.py | 28 +++++++++++++++++--- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/agentops/helpers/serialization.py b/agentops/helpers/serialization.py index 910fcdec3..ad51295b4 100644 --- a/agentops/helpers/serialization.py +++ b/agentops/helpers/serialization.py @@ -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 diff --git a/tests/unit/test_serialization.py b/tests/unit/test_serialization.py index bab7550c9..4a263c1ea 100644 --- a/tests/unit/test_serialization.py +++ b/tests/unit/test_serialization.py @@ -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.""" @@ -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")