diff --git a/py/src/braintrust/framework.py b/py/src/braintrust/framework.py index a1fe7742..091f65e5 100644 --- a/py/src/braintrust/framework.py +++ b/py/src/braintrust/framework.py @@ -1324,6 +1324,19 @@ def _build_classification_span_output( return grouped_output +def _normalize_score(value: Any, error_message: str) -> ScoreLike: + original_value = value + if isinstance(value, dict): + try: + value = Score.from_dict(value) + except Exception as e: + raise ValueError(f"{error_message} Got: {original_value}") from e + + if not is_score(value): + raise ValueError(f"{error_message} Got: {original_value}") + return value + + def _validate_classification_result(value: Any, classifier_name: str) -> Classification: if isinstance(value, Mapping): if not value: @@ -1455,18 +1468,16 @@ async def await_or_run_scorer(root_span, scorer, name, **kwargs): result = await call_user_fn(event_loop, score, **kwargs) if isinstance(result, dict): - try: - result = Score.from_dict(result) - except Exception as e: - raise ValueError(f"When returning a dict, it must be a valid Score object. Got: {result}") from e + result = _normalize_score(result, "When returning a dict, it must be a valid Score object.") if isinstance(result, Iterable) and not isinstance(result, (str, bytes, Mapping)): - for s in result: - if not is_score(s): - raise ValueError( - f"When returning an array of scores, each score must be a valid Score object. Got: {s}" - ) - result = list(result) + result = [ + _normalize_score( + score, + "When returning an array of scores, each score must be a valid Score object.", + ) + for score in result + ] elif is_score(result): result = [result] else: diff --git a/py/src/braintrust/test_framework.py b/py/src/braintrust/test_framework.py index e204c4c5..5c000bdf 100644 --- a/py/src/braintrust/test_framework.py +++ b/py/src/braintrust/test_framework.py @@ -296,6 +296,46 @@ def _run_eval_sync(self, *args, **kwargs): assert result.summary.scores[scorer_name].score == 1.0 +@pytest.mark.asyncio +async def test_run_evaluator_normalizes_list_of_dict_scores(): + data = [ + EvalCase(input=1, expected=1), + EvalCase(input=2, expected=2), + ] + + def simple_task(input_value): + return input_value + + def list_dict_scorer(input_value, output, expected): + return [ + {"name": "per_result", "score": 1.0}, + {"name": "summary_only", "score": 1.0}, + ] + + evaluator = Evaluator( + project_name="test-project", + eval_name="test-list-dict-scorer", + data=data, + task=simple_task, + scores=[list_dict_scorer], + experiment_name=None, + metadata=None, + ) + + result = await run_evaluator(None, evaluator, None, []) + + assert isinstance(result, EvalResultWithSummary) + assert len(result.results) == 2 + + for eval_result in result.results: + assert eval_result.scores["per_result"] == 1.0 + assert eval_result.scores["summary_only"] == 1.0 + + assert result.summary.project_name == "test-project" + assert result.summary.scores["per_result"].score == 1.0 + assert result.summary.scores["summary_only"].score == 1.0 + + @pytest.mark.asyncio @pytest.mark.skipif( sys.version_info < (3, 14),