fix(google_genai): keep skipped results after answers split by empty or text turns

A skipped result now goes after the last answer before the next model turn. An empty or text-only user turn between split answers used to end the search early, so GPT read the later answer as the skipped call's result
This commit is contained in:
mateo-berri 2026-11-10 08:10:08 -08:00
parent f039b97fa6
commit 2e3eda2d27
2 changed files with 30 additions and 4 deletions

View file

@ -424,7 +424,10 @@ def _genai_turn_messages(
def _last_answering_turn(turns: Sequence[_GenAITurn], index: int) -> int: def _last_answering_turn(turns: Sequence[_GenAITurn], index: int) -> int:
return next((later for later in range(index + 1, len(turns)) if not turns[later].responses), len(turns)) - 1 next_model_turn: Final = next(
(later for later in range(index + 1, len(turns)) if turns[later].role == "model"), len(turns)
)
return max((later for later in range(index + 1, next_model_turn) if turns[later].responses), default=index)
def _genai_contents_messages(contents: Sequence[object]) -> tuple[AllMessageValues, ...]: def _genai_contents_messages(contents: Sequence[object]) -> tuple[AllMessageValues, ...]:

View file

@ -660,9 +660,11 @@ def _call_tags(messages: list[dict[str, object]]) -> dict[str, str]:
} }
def _ids_answered_right_after(messages: list[dict[str, object]], index: int) -> list[str]: def _ids_answered_before_the_next_model_turn(messages: list[dict[str, object]], index: int) -> list[str]:
return [ return [
answer["tool_call_id"] for answer in takewhile(lambda message: message["role"] == "tool", messages[index + 1 :]) message["tool_call_id"]
for message in takewhile(lambda message: message["role"] != "assistant", messages[index + 1 :])
if message["role"] == "tool"
] ]
@ -716,6 +718,25 @@ def _ids_answered_right_after(messages: list[dict[str, object]], index: int) ->
frozenset({"paris", "rome"}), frozenset({"paris", "rome"}),
id="answers_split_across_user_turns", id="answers_split_across_user_turns",
), ),
pytest.param(
[
{
"role": "model",
"parts": [
_tagged_call("get_weather", "paris"),
_tagged_call("get_weather", "rome"),
_tagged_call("get_weather", "oslo"),
],
},
{"role": "user", "parts": [_tagged_response("get_weather", "paris")]},
{"role": "user", "parts": []},
{"role": "user", "parts": [{"text": "Rome took longer"}]},
{"role": "user", "parts": [_tagged_response("get_weather", "rome")]},
{"role": "model", "parts": [{"text": "Oslo did not answer"}]},
],
frozenset({"paris", "rome"}),
id="answers_split_by_empty_and_text_turns",
),
], ],
) )
def test_a_call_a_later_turn_left_unanswered_gets_one_skipped_result_and_the_others_keep_their_answers( def test_a_call_a_later_turn_left_unanswered_gets_one_skipped_result_and_the_others_keep_their_answers(
@ -732,7 +753,9 @@ def test_a_call_a_later_turn_left_unanswered_gets_one_skipped_result_and_the_oth
if message.get("tool_calls") if message.get("tool_calls")
} }
answers: Final = [message for message in messages if message["role"] == "tool"] answers: Final = [message for message in messages if message["role"] == "tool"]
assert {index: _ids_answered_right_after(messages, index) for index in call_ids_per_turn} == call_ids_per_turn assert {
index: _ids_answered_before_the_next_model_turn(messages, index) for index in call_ids_per_turn
} == call_ids_per_turn
assert { assert {
tag_by_call_id[answer["tool_call_id"]]: json.loads(answer["content"])["tag"] tag_by_call_id[answer["tool_call_id"]]: json.loads(answer["content"])["tag"]
for answer in answers for answer in answers