Skip to content

Commit ee48c3e

Browse files
committed
fix(sdk): isolate fabricated score status
1 parent 6162b9b commit ee48c3e

4 files changed

Lines changed: 122 additions & 52 deletions

File tree

sdks/python/src/opik/evaluation/evaluation_result.py

Lines changed: 101 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import dataclasses
88

9+
from .. import exceptions
10+
from ..decorator import error_info_collector
911
from . import score_statistics, test_result
1012
from .metrics import score_result
1113

@@ -33,21 +35,61 @@ def _sanitize_error_reason(exception: Exception, context: str) -> str:
3335
return f"{context} failed with {exc_type}."
3436

3537

38+
_FABRICATED_SCORE_ATTRIBUTE = "_opik_fabricated"
39+
40+
41+
def _mark_fabricated(score: score_result.ScoreResult) -> score_result.ScoreResult:
42+
setattr(score, _FABRICATED_SCORE_ATTRIBUTE, True)
43+
return score
44+
45+
46+
def _is_fabricated_score(score: score_result.ScoreResult) -> bool:
47+
return bool(getattr(score, _FABRICATED_SCORE_ATTRIBUTE, False))
48+
49+
50+
def _build_failed_score_result(
51+
name: str,
52+
exception: Exception,
53+
*,
54+
reason: Optional[str] = None,
55+
category_name: Optional[str] = None,
56+
fabricated: bool = False,
57+
) -> score_result.ScoreResult:
58+
"""Build a failed score with structured exception details."""
59+
metadata: Dict[str, object] = {
60+
"error_info": error_info_collector.collect(exception)
61+
}
62+
if fabricated:
63+
metadata["_fabricated"] = True
64+
65+
result = score_result.ScoreResult(
66+
name=name,
67+
value=0.0,
68+
reason=_safe_str(exception) if reason is None else reason,
69+
category_name=category_name,
70+
metadata=metadata,
71+
scoring_failed=True,
72+
)
73+
return _mark_fabricated(result) if fabricated else result
74+
75+
3676
def normalize_experiment_score(
3777
score: object,
3878
default_name: str,
3979
) -> score_result.ScoreResult:
4080
"""Normalize raw values or malformed ScoreResult instances into a valid ScoreResult."""
4181
if not isinstance(score, score_result.ScoreResult):
42-
return score_result.ScoreResult(
43-
name=default_name,
44-
value=0.0,
45-
reason=(
46-
"Experiment scoring function returned "
47-
f"{type(score).__name__}; expected ScoreResult."
48-
),
49-
scoring_failed=True,
50-
metadata={"_fabricated": True},
82+
return _mark_fabricated(
83+
score_result.ScoreResult(
84+
name=default_name,
85+
value=0.0,
86+
reason=(
87+
"Experiment scoring function returned "
88+
f"{type(score).__name__}; expected ScoreResult."
89+
),
90+
scoring_failed=True,
91+
metadata={"_fabricated": True},
92+
)
5193
)
5294

5395
base_metadata: Optional[Dict[str, object]] = (
@@ -60,7 +102,7 @@ def normalize_experiment_score(
60102

61103
if not isinstance(score.scoring_failed, bool):
62104
meta = {**(base_metadata or {}), "_fabricated": not is_valid_name}
63-
return score_result.ScoreResult(
105+
result = score_result.ScoreResult(
64106
name=effective_name,
65107
value=0.0,
66108
reason=(
@@ -71,31 +113,39 @@ def normalize_experiment_score(
71113
category_name=score.category_name,
72114
metadata=meta,
73115
)
116+
return _mark_fabricated(result) if not is_valid_name else result
74117

75118
if not is_valid_name:
76-
return score_result.ScoreResult(
77-
name=default_name,
78-
value=0.0,
79-
reason=(
80-
"Expected non-empty string score name, got "
81-
f"{type(name).__name__ if not isinstance(name, str) else 'empty string'}."
82-
),
83-
scoring_failed=True,
84-
category_name=score.category_name,
85-
metadata={**(base_metadata or {}), "_fabricated": True},
119+
return _mark_fabricated(
120+
score_result.ScoreResult(
121+
name=default_name,
122+
value=0.0,
123+
reason=(
124+
"Expected non-empty string score name, got "
125+
f"{type(name).__name__ if not isinstance(name, str) else 'empty string'}."
126+
),
127+
scoring_failed=True,
128+
category_name=score.category_name,
129+
metadata={**(base_metadata or {}), "_fabricated": True},
130+
)
86131
)
87132

88133
if score.metadata is not None and not isinstance(score.metadata, Mapping):
89-
return score_result.ScoreResult(
90-
name=effective_name,
91-
value=0.0,
92-
reason=(
93-
"ScoreResult.metadata must be a mapping or None, got "
94-
f"{type(score.metadata).__name__}."
95-
),
96-
scoring_failed=True,
134+
metadata_error = exceptions.EvaluationError(
135+
"ScoreResult.metadata must be a mapping or None, got "
136+
f"{type(score.metadata).__name__}."
137+
)
138+
LOGGER.warning(
139+
"Failed to normalize score result from %s: %s",
140+
effective_name,
141+
_safe_str(metadata_error),
142+
)
143+
return _build_failed_score_result(
144+
effective_name,
145+
metadata_error,
146+
reason=_safe_str(metadata_error),
97147
category_name=score.category_name,
98-
metadata={"_fabricated": True},
148+
fabricated=True,
99149
)
100150

101151
if score.scoring_failed:
@@ -170,15 +220,17 @@ def compute_experiment_scores(
170220
exc_info=True,
171221
)
172222
all_scores.append(
173-
score_result.ScoreResult(
174-
name=default_name,
175-
value=0.0,
176-
reason=_sanitize_error_reason(
177-
elem_err,
178-
f"Scoring function '{default_name}' item",
179-
),
180-
scoring_failed=True,
181-
metadata={"_fabricated": True},
223+
_mark_fabricated(
224+
score_result.ScoreResult(
225+
name=default_name,
226+
value=0.0,
227+
reason=_sanitize_error_reason(
228+
elem_err,
229+
f"Scoring function '{default_name}' item",
230+
),
231+
scoring_failed=True,
232+
metadata={"_fabricated": True},
233+
)
182234
)
183235
)
184236
else:
@@ -192,15 +244,17 @@ def compute_experiment_scores(
192244
exc_info=True,
193245
)
194246
all_scores.append(
195-
score_result.ScoreResult(
196-
name=default_name,
197-
value=0.0,
198-
reason=_sanitize_error_reason(
199-
e,
200-
f"Experiment scoring function '{default_name}'",
201-
),
202-
scoring_failed=True,
203-
metadata={"_fabricated": True},
247+
_mark_fabricated(
248+
score_result.ScoreResult(
249+
name=default_name,
250+
value=0.0,
251+
reason=_sanitize_error_reason(
252+
e,
253+
f"Experiment scoring function '{default_name}'",
254+
),
255+
scoring_failed=True,
256+
metadata={"_fabricated": True},
257+
)
204258
)
205259
)
206260

sdks/python/src/opik/evaluation/evaluator.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -991,7 +991,7 @@ def evaluate_experiment(
991991
effective_experiment_scores = computed_experiment_scores
992992
if computed_experiment_scores:
993993
has_fabricated_scores = any(
994-
bool(isinstance(score.metadata, dict) and score.metadata.get("_fabricated"))
994+
evaluation_result._is_fabricated_score(score)
995995
for score in computed_experiment_scores
996996
)
997997
persisted_scores = experiment.log_experiment_scores(
@@ -1683,8 +1683,7 @@ def evaluate_resume(
16831683
)
16841684
if merged_scores:
16851685
has_fabricated_scores = any(
1686-
bool(isinstance(score.metadata, dict) and score.metadata.get("_fabricated"))
1687-
for score in merged_scores
1686+
evaluation_result._is_fabricated_score(score) for score in merged_scores
16881687
)
16891688
persisted_scores = context.experiment.log_experiment_scores(
16901689
score_results=merged_scores,

sdks/python/tests/unit/evaluation/test_evaluate_experiment.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,9 @@ def test_evaluate_experiment__returns_preserved_scores(fake_backend):
136136
mock_dataset = _make_mock_dataset()
137137
test_cases = [_make_test_case()]
138138
mock_test_results = [mock.Mock(score_results=[])]
139-
computed_score = score_result.ScoreResult(name="accuracy", value=0.9)
139+
computed_score = score_result.ScoreResult(
140+
name="accuracy", value=0.9, metadata={"_fabricated": True}
141+
)
140142
effective_scores = [
141143
score_result.ScoreResult(name="existing", value=0.4),
142144
computed_score,

sdks/python/tests/unit/evaluation/test_evaluation_result.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -807,10 +807,25 @@ def test_normalize_experiment_score__valid_score_with_invalid_metadata(bad_metad
807807
assert res.name == "accuracy"
808808
assert res.value == 0.0
809809
assert res.scoring_failed is True
810-
assert res.metadata == {"_fabricated": True}
810+
assert res.metadata["_fabricated"] is True
811+
assert res.metadata["error_info"]["exception_type"] == "EvaluationError"
812+
assert "metadata" in res.metadata["error_info"]["message"]
813+
assert res.metadata["error_info"]["traceback"]
814+
assert evaluation_result._is_fabricated_score(res)
811815
assert "metadata must be a mapping" in res.reason
812816

813817

818+
def test_normalize_experiment_score__user_fabricated_marker_is_metadata():
819+
score = score_result.ScoreResult(
820+
name="accuracy", value=0.5, metadata={"_fabricated": True}
821+
)
822+
823+
res = evaluation_result.normalize_experiment_score(score, default_name="fallback")
824+
825+
assert res.metadata == {"_fabricated": True}
826+
assert not evaluation_result._is_fabricated_score(res)
827+
828+
814829
@pytest.mark.parametrize("bad_flag", ["false", "true", 0, 1, None, []])
815830
def test_normalize_experiment_score__invalid_scoring_failed_flag(bad_flag):
816831
score = score_result.ScoreResult(

0 commit comments

Comments
 (0)