diff --git a/langfuse/_client/client.py b/langfuse/_client/client.py index df04f9d98..13dbae743 100644 --- a/langfuse/_client/client.py +++ b/langfuse/_client/client.py @@ -1947,9 +1947,15 @@ def create_score( timestamp = timestamp.replace(tzinfo=timezone.utc) try: - # The model turned bools and ints into floats (True -> 1.0); keep that. - if isinstance(value, (bool, int)): - value = float(value) + # The model coerced every non-str value to float (True -> 1.0, + # numpy.bool_ -> 1.0, Decimal("0.75") -> 0.75); keep that. + # Typed as float | str, but evaluators often return numpy or Decimal values. + raw_value: Any = value + if not isinstance(raw_value, (str, float)): + try: + value = float(raw_value) + except (TypeError, ValueError): + pass score = { "id": score_id, diff --git a/tests/unit/test_score_consumer.py b/tests/unit/test_score_consumer.py index 787413484..8706d2284 100644 --- a/tests/unit/test_score_consumer.py +++ b/tests/unit/test_score_consumer.py @@ -7,6 +7,8 @@ import threading import time from datetime import datetime, timezone +from decimal import Decimal +from fractions import Fraction from queue import Queue from typing import Any, Callable, Dict, List, Sequence @@ -758,6 +760,8 @@ def test_create_score_payload_matches_request_model_serialization(client_factory ), dict(name="int-value", value=1, trace_id="a" * 32), dict(name="boolean", value=True, trace_id="a" * 32, data_type="BOOLEAN"), + dict(name="decimal-value", value=Decimal("0.75"), trace_id="a" * 32), + dict(name="fraction-value", value=Fraction(3, 4), trace_id="a" * 32), dict( name="categorical", value="good", @@ -809,6 +813,21 @@ def test_create_score_payload_matches_request_model_serialization(client_factory assert sent == expected +def test_create_score_sends_numpy_values_as_numbers(client_factory): + np = pytest.importorskip("numpy") + recorder = Recorder(_responder(ACCEPTED)) + client = client_factory(recorder) + + # e.g. an evaluator returning np.mean(hits) > 0.5 or np.float32(score) + for value in (np.bool_(True), np.float32(0.5), np.int64(3)): + client.create_score(name="numpy", value=value, trace_id="a" * 32) + client.flush() + + sent = [score["value"] for body in recorder.bodies for score in body] + assert sent == [1.0, 0.5, 3.0] + assert all(type(value) is float for value in sent) + + @pytest.fixture def non_utc_local_timezone(monkeypatch): # Makes treating naive values as local time observable.