@@ -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+
6595def _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
584632class 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