Skip to content

Commit 1ffa74f

Browse files
speedstorm1copybara-github
authored andcommitted
fix: Drop the whole AFC exchange from curated chat history when the final model turn is invalid
fixes #3314 FUTURE_COPYBARA_INTEGRATE_REVIEW=#3249 from googleapis:release-please--branches--main 4b4b0f3 PiperOrigin-RevId: 1016479900
1 parent afb10e8 commit 1ffa74f

2 files changed

Lines changed: 352 additions & 15 deletions

File tree

‎google/genai/chats.py‎

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,36 @@ def _validate_response(response: GenerateContentResponse) -> bool:
6262
return _validate_content(response.candidates[0].content)
6363

6464

65+
def _has_function_call(content: Content) -> bool:
66+
return any(part.function_call is not None for part in content.parts or [])
67+
68+
69+
def _has_function_response(content: Content) -> bool:
70+
return any(part.function_response is not None for part in content.parts or [])
71+
72+
73+
def _drop_rejected_input(curated_history: list[Content]) -> None:
74+
"""Drops the input of a rejected turn from the end of the curated history.
75+
76+
If that input is a function response, the model turn holding the call it
77+
answers is dropped too, and so is the input before that call. This repeats
78+
back to the input that started the exchange.
79+
80+
Args:
81+
curated_history: The curated history to trim in place.
82+
"""
83+
while curated_history:
84+
rejected_input = curated_history.pop()
85+
if not _has_function_response(rejected_input):
86+
return
87+
call_start = len(curated_history)
88+
while call_start > 0 and curated_history[call_start - 1].role == "model":
89+
call_start -= 1
90+
if not any(_has_function_call(c) for c in curated_history[call_start:]):
91+
return
92+
del curated_history[call_start:]
93+
94+
6595
def _extract_curated_history(
6696
comprehensive_history: list[Content],
6797
) -> list[Content]:
@@ -104,8 +134,8 @@ def _extract_curated_history(
104134
i += 1
105135
if is_valid:
106136
curated_history.extend(current_output)
107-
elif curated_history:
108-
curated_history.pop()
137+
else:
138+
_drop_rejected_input(curated_history)
109139
return curated_history
110140

111141

@@ -166,6 +196,18 @@ def record_history(
166196
self._curated_history.extend(input_contents)
167197
self._curated_history.extend(output_contents)
168198

199+
def _drop_afc_exchange(self, curated_length: int) -> None:
200+
"""Drops a rejected AFC exchange from the curated history.
201+
202+
AFC records each function call and its response as it runs. If the final
203+
model output is invalid, the whole exchange is dropped, as the input of
204+
any invalid turn is.
205+
206+
Args:
207+
curated_length: The curated history length before the exchange began.
208+
"""
209+
del self._curated_history[curated_length:]
210+
169211
def get_history(self, curated: bool = False) -> list[Content]:
170212
"""Returns the chat history.
171213
@@ -334,6 +376,7 @@ def send_message(
334376
response = types.GenerateContentResponse()
335377
function_map = _extra_utils.get_function_map(parsed_config)
336378
i = 0
379+
curated_length = len(self._curated_history)
337380
while remaining_remote_calls_afc > 0:
338381
i += 1
339382
response = self._modules.generate_content(
@@ -383,11 +426,14 @@ def send_message(
383426
if response.candidates and response.candidates[0].content
384427
else []
385428
)
429+
is_valid = _validate_response(response)
386430
self.record_history(
387431
user_input=user_input,
388432
model_output=model_output,
389-
is_valid=_validate_response(response),
433+
is_valid=is_valid,
390434
)
435+
if not is_valid:
436+
self._drop_afc_exchange(curated_length)
391437
return response
392438

393439
def send_message_stream(
@@ -503,6 +549,7 @@ def send_message_stream(
503549
function_map = _extra_utils.get_function_map(parsed_config)
504550
i = 0
505551
if isinstance(self._modules, Models):
552+
curated_length = len(self._curated_history)
506553
while remaining_remote_calls_afc > 0:
507554
i += 1
508555
response_stream = self._modules.generate_content_stream(
@@ -570,15 +617,16 @@ def send_message_stream(
570617
)
571618
user_input = func_response_content
572619

620+
is_valid = bool(
621+
is_valid and model_output is not None and finish_reason is not None
622+
)
573623
self.record_history(
574624
user_input=user_input,
575625
model_output=model_output,
576-
is_valid=bool(
577-
is_valid
578-
and model_output is not None
579-
and finish_reason is not None
580-
),
626+
is_valid=is_valid,
581627
)
628+
if not is_valid:
629+
self._drop_afc_exchange(curated_length)
582630

583631

584632
class Chats:
@@ -822,6 +870,7 @@ async def send_message(
822870
)
823871

824872
i = 0
873+
curated_length = len(self._curated_history)
825874
while remaining_remote_calls_afc > 0:
826875
i += 1
827876
response = await self._modules.generate_content(
@@ -878,11 +927,14 @@ async def send_message(
878927
if response.candidates and response.candidates[0].content
879928
else []
880929
)
930+
is_valid = _validate_response(response)
881931
self.record_history(
882932
user_input=user_input,
883933
model_output=model_output,
884-
is_valid=_validate_response(response),
934+
is_valid=is_valid,
885935
)
936+
if not is_valid:
937+
self._drop_afc_exchange(curated_length)
886938
return response
887939

888940
async def send_message_stream(
@@ -1075,6 +1127,7 @@ async def async_generator(): # type: ignore[no-untyped-def]
10751127
finish_reason = None
10761128
is_valid = True
10771129

1130+
curated_length = len(self._curated_history)
10781131
while remaining_remote_calls_afc > 0:
10791132
i += 1
10801133
response_stream = await self._modules.generate_content_stream(
@@ -1144,15 +1197,14 @@ async def async_generator(): # type: ignore[no-untyped-def]
11441197
)
11451198
user_input = func_response_content
11461199

1200+
is_valid = bool(is_valid and model_output and finish_reason is not None)
11471201
self.record_history(
11481202
user_input=user_input,
11491203
model_output=model_output,
1150-
is_valid=bool(
1151-
is_valid
1152-
and model_output
1153-
and finish_reason is not None
1154-
),
1204+
is_valid=is_valid,
11551205
)
1206+
if not is_valid:
1207+
self._drop_afc_exchange(curated_length)
11561208

11571209
return async_generator() # type: ignore[no-untyped-call, no-any-return]
11581210

0 commit comments

Comments
 (0)