From 3fec6261afaa139666d376881e140b07dfa7db16 Mon Sep 17 00:00:00 2001 From: Anas Khan <83116240+anxkhn@users.noreply.github.com> Date: Sat, 4 Jul 2026 22:52:00 +0530 Subject: [PATCH] fix(evaluation): avoid crash on first turn with empty user content `_evaluate_first_turn` guarded only against a `None` `user_content`, then called `get_text_from_content(...).strip()`. When `user_content` is a non-None `Content` whose parts are empty or `None`, `get_text_from_content` returns `None`, so `.strip()` raised `AttributeError: 'NoneType' object has no attribute 'strip'`. Compute the extracted text once and return a `NOT_EVALUATED` result when it is `None`, mirroring the existing guard for a `None` `user_content` above it and the `None`/empty guard in `_convert_llm_response_to_score`. Add a regression test covering `parts=[]` and `parts=None`. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com> --- .../per_turn_user_simulator_quality_v1.py | 10 +++++-- ...est_per_turn_user_simulation_quality_v1.py | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/google/adk/evaluation/simulation/per_turn_user_simulator_quality_v1.py b/src/google/adk/evaluation/simulation/per_turn_user_simulator_quality_v1.py index 239fa31a715..8d2a88dda4d 100644 --- a/src/google/adk/evaluation/simulation/per_turn_user_simulator_quality_v1.py +++ b/src/google/adk/evaluation/simulation/per_turn_user_simulator_quality_v1.py @@ -302,9 +302,15 @@ def _evaluate_first_turn( eval_status=EvalStatus.NOT_EVALUATED, ) + user_text = get_text_from_content(first_invocation.user_content) + if user_text is None: + return PerInvocationResult( + actual_invocation=first_invocation, + eval_status=EvalStatus.NOT_EVALUATED, + ) + score = int( - get_text_from_content(first_invocation.user_content).strip() - == conversation_scenario.starting_prompt.strip() + user_text.strip() == conversation_scenario.starting_prompt.strip() ) return PerInvocationResult( actual_invocation=first_invocation, diff --git a/tests/unittests/evaluation/simulation/test_per_turn_user_simulation_quality_v1.py b/tests/unittests/evaluation/simulation/test_per_turn_user_simulation_quality_v1.py index be0a0394c54..296b9935c6a 100644 --- a/tests/unittests/evaluation/simulation/test_per_turn_user_simulation_quality_v1.py +++ b/tests/unittests/evaluation/simulation/test_per_turn_user_simulation_quality_v1.py @@ -514,6 +514,32 @@ def test_evaluate_first_turn_failure(): assert result.eval_status == EvalStatus.FAILED +@pytest.mark.parametrize( + "user_content", + [ + types.Content(role="user", parts=[]), + types.Content(), + ], +) +def test_evaluate_first_turn_not_evaluated_when_user_content_has_no_text( + user_content, +): + """First turn with empty/None parts is not evaluated instead of crashing.""" + evaluator = _create_test_evaluator( + threshold=1.0, stop_signal="test stop signal" + ) + conversation_scenario = _create_test_conversation_scenario( + conversation_plan="plan", + starting_prompt="test starting prompt", + ) + invocation = Invocation(invocation_id="1", user_content=user_content) + + result = evaluator._evaluate_first_turn(invocation, conversation_scenario) + + assert result.score is None + assert result.eval_status == EvalStatus.NOT_EVALUATED + + def test_aggregate_conversation_results_all_pass_produces_pass(): evaluator = _create_test_evaluator() results = [