diff --git a/langfuse/_client/propagation.py b/langfuse/_client/propagation.py index 597d8126e..5d485249d 100644 --- a/langfuse/_client/propagation.py +++ b/langfuse/_client/propagation.py @@ -90,12 +90,11 @@ def _detach_context_token_safely(token: Any) -> None: except Exception: pass - def propagate_attributes( *, user_id: Optional[str] = None, session_id: Optional[str] = None, - metadata: Optional[Dict[str, str]] = None, + metadata: Optional[Dict[str, Any]] = None, version: Optional[str] = None, tags: Optional[List[str]] = None, trace_name: Optional[str] = None, @@ -229,7 +228,7 @@ def _propagate_attributes( *, user_id: Optional[str] = None, session_id: Optional[str] = None, - metadata: Optional[Dict[str, str]] = None, + metadata: Optional[Dict[str, Any]] = None, version: Optional[str] = None, tags: Optional[List[str]] = None, trace_name: Optional[str] = None, @@ -247,10 +246,9 @@ def _propagate_attributes( "trace_name": trace_name, } - propagated_metadata_attributes: Dict[str, Optional[Dict[str, str]]] = { + propagated_metadata_attributes: Dict[str, Optional[Dict[str, Any]]] = { "metadata": metadata, } - if experiment: for key, value in experiment.items(): if key in ("experiment_metadata", "experiment_item_metadata"): @@ -286,8 +284,11 @@ def _propagate_attributes( validated_metadata: Dict[str, str] = {} for key, value in metadata_value.items(): - if _validate_string_value(value=value, key=f"{metadata_key}.{key}"): - validated_metadata[key] = value + if value is None: + continue + string_value = value if isinstance(value, str) else str(value) + if _validate_string_value(value=string_value, key=f"{metadata_key}.{key}"): + validated_metadata[key] = string_value if validated_metadata: context = _set_propagated_attribute( diff --git a/langfuse/_utils/serializer.py b/langfuse/_utils/serializer.py index 24c19aa12..27294bf80 100644 --- a/langfuse/_utils/serializer.py +++ b/langfuse/_utils/serializer.py @@ -76,7 +76,7 @@ def _default_inner(self, obj: Any) -> Any: return "NaN" if isinstance(obj, float) and math.isinf(obj): - return "Infinity" + return "-Infinity" if obj < 0 else "Infinity" if isinstance(obj, (Exception, KeyboardInterrupt)): return f"{type(obj).__name__}: {str(obj)}" diff --git a/tests/unit/test_propagate_attributes.py b/tests/unit/test_propagate_attributes.py index c783e65dd..a77eceffe 100644 --- a/tests/unit/test_propagate_attributes.py +++ b/tests/unit/test_propagate_attributes.py @@ -489,7 +489,36 @@ def test_mixed_valid_invalid_metadata(self, langfuse_client, memory_exporter): self.verify_missing_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.invalid_key" ) + def test_metadata_coercion_of_non_string_values(self, langfuse_client, memory_exporter): + """Verify metadata values that are not strings (int, float, bool) are coerced to strings.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + metadata={ + "int_val": 42, # type: ignore + "float_val": 3.14, # type: ignore + "bool_val": True, # type: ignore + } + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + # Verify child has all metadata keys coerced to string values + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.int_val", + "42", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.float_val", + "3.14", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.bool_val", + "True", + ) class TestPropagateAttributesNesting(TestPropagateAttributesBase): """Tests for nested propagate_attributes contexts.""" diff --git a/tests/unit/test_serializer.py b/tests/unit/test_serializer.py index ce9462e04..4c556822f 100644 --- a/tests/unit/test_serializer.py +++ b/tests/unit/test_serializer.py @@ -14,17 +14,17 @@ ) -class TestEnum(Enum): +class MockEnum(Enum): A = 1 B = 2 @dataclass -class TestDataclass: +class MockDataclass: field: str -class TestBaseModel(BaseModel): +class MockBaseModel(BaseModel): field: str @@ -43,7 +43,7 @@ def test_date(): def test_enum(): serializer = EventSerializer() - assert serializer.encode(TestEnum.A) == "1" + assert serializer.encode(MockEnum.A) == "1" def test_uuid(): @@ -59,13 +59,12 @@ def test_bytes(): def test_dataclass(): - dc = TestDataclass(field="test") + dc = MockDataclass(field="test") serializer = EventSerializer() assert json.loads(serializer.encode(dc)) == {"field": "test"} - def test_pydantic_model(): - model = TestBaseModel(field="test") + model = MockBaseModel(field="test") serializer = EventSerializer() assert json.loads(serializer.encode(model)) == {"field": "test"} @@ -298,3 +297,11 @@ def test_dict_with_non_string_keys_is_serialized(input_obj, expected): result = json.loads(EventSerializer().encode(input_obj)) assert result == expected + + +def test_float_special_values(): + serializer = EventSerializer() + + assert serializer.encode(float("inf")) == '"Infinity"' + assert serializer.encode(float("-inf")) == '"-Infinity"' + assert serializer.encode(float("nan")) == '"NaN"' \ No newline at end of file