From f7c7f2a0ad93aa59ff47cda4281cd3fe72491f48 Mon Sep 17 00:00:00 2001 From: Yvonne Yu Date: Mon, 3 Aug 2026 16:48:26 -0700 Subject: [PATCH] feat: introduce ChatConfig for automatic function calling in chat experience. This is not a breaking change, but it is in preparation of the breaking change for automatic function calling in the next major version update. PiperOrigin-RevId: 958647476 --- google/genai/chats.py | 98 ++++++++++++++++--- google/genai/models.py | 34 +++++++ google/genai/tests/chats/test_send_message.py | 27 +++++ google/genai/types.py | 33 +++++++ 4 files changed, 180 insertions(+), 12 deletions(-) diff --git a/google/genai/chats.py b/google/genai/chats.py index 8ab765db0..d3e7c1487 100644 --- a/google/genai/chats.py +++ b/google/genai/chats.py @@ -22,7 +22,16 @@ from . import _transformers as t from . import types from .models import AsyncModels, Models -from .types import Content, ContentOrDict, GenerateContentConfigOrDict, GenerateContentResponse, Part, PartUnionDict +from .types import ( + ChatConfig, + Content, + ContentOrDict, + GenerateContentConfig, + GenerateContentConfigOrDict, + GenerateContentResponse, + Part, + PartUnionDict, +) if sys.version_info >= (3, 10): @@ -104,6 +113,21 @@ def _extract_curated_history( return curated_history +def _extract_generate_content_config( + chat_config: Optional[ChatConfig] = None, +) -> Optional[GenerateContentConfig]: + """Slices a ChatConfig instance to a GenerateContentConfig instance. + """ + if not chat_config: + return None + + generate_content_config_kwargs = { + field: getattr(chat_config, field) + for field in GenerateContentConfig.model_fields + } + return GenerateContentConfig(**generate_content_config_kwargs) + + class _BaseChat: """Base chat session.""" @@ -111,13 +135,18 @@ def __init__( self, *, model: str, - config: Optional[GenerateContentConfigOrDict] = None, + config: Optional[Union[GenerateContentConfigOrDict, ChatConfig]] = None, history: list[ContentOrDict], ): self._model = model - self._config = _extra_utils.get_usage_header( + if isinstance(config, ChatConfig): + self._config = _extra_utils.get_usage_header( + config, types.ChatConfig, usage="chat", # type: ignore[arg-type] + ) + else: + self._config = _extra_utils.get_usage_header( config, types.GenerateContentConfig, usage="chat" # type: ignore[arg-type] - ) + ) content_models = [] for content in history: if not isinstance(content, Content): @@ -214,7 +243,7 @@ def __init__( *, modules: Models, model: str, - config: Optional[GenerateContentConfigOrDict] = None, + config: Optional[Union[GenerateContentConfigOrDict, ChatConfig]] = None, history: list[ContentOrDict], ): self._modules = modules @@ -227,7 +256,7 @@ def __init__( def send_message( self, message: Union[list[PartUnionDict], PartUnionDict], - config: Optional[GenerateContentConfigOrDict] = None, + config: Optional[Union[GenerateContentConfigOrDict, ChatConfig]] = None, ) -> GenerateContentResponse: """Sends the conversation history with the additional message and returns the model's response. @@ -254,6 +283,18 @@ def send_message( ) input_content = t.t_content(message) method_config = config if config else self._config + + # tmp workaround before the next major version update + # extract afc config from ChatConfig and merge it into GenerateContentConfig + if isinstance(method_config, ChatConfig): + afc_config = method_config.automatic_function_calling_config + if afc_config.enable is not None: + afc_config.disable = not afc_config.enable + gc_config = _extract_generate_content_config(method_config) + gc_config.automatic_function_calling = afc_config + method_config = gc_config + + method_config = _extra_utils.get_usage_header( method_config, types.GenerateContentConfig, usage="chat" # type: ignore[arg-type] ) @@ -283,7 +324,7 @@ def send_message( def send_message_stream( self, message: Union[list[PartUnionDict], PartUnionDict], - config: Optional[GenerateContentConfigOrDict] = None, + config: Optional[Union[GenerateContentConfigOrDict, ChatConfig]] = None, ) -> Iterator[GenerateContentResponse]: """Sends the conversation history with the additional message and yields the model's response in chunks. @@ -315,6 +356,17 @@ def send_message_stream( is_valid = True chunk = None method_config = config if config else self._config + + # tmp workaround before the next major version update + # extract afc config from ChatConfig and merge it into GenerateContentConfig + if isinstance(method_config, ChatConfig): + afc_config = method_config.automatic_function_calling_config + if afc_config.enable is not None: + afc_config.disable = not afc_config.enable + gc_config = _extract_generate_content_config(method_config) + gc_config.automatic_function_calling = afc_config + method_config = gc_config + method_config = _extra_utils.get_usage_header( method_config, types.GenerateContentConfig, usage="chat" # type: ignore[arg-type] ) @@ -356,7 +408,7 @@ def create( self, *, model: str, - config: Optional[GenerateContentConfigOrDict] = None, + config: Optional[Union[GenerateContentConfigOrDict, ChatConfig]] = None, history: Optional[list[ContentOrDict]] = None, ) -> Chat: """Creates a new chat session. @@ -385,7 +437,7 @@ def __init__( *, modules: AsyncModels, model: str, - config: Optional[GenerateContentConfigOrDict] = None, + config: Optional[Union[GenerateContentConfigOrDict, ChatConfig]] = None, history: list[ContentOrDict], ): self._modules = modules @@ -398,7 +450,7 @@ def __init__( async def send_message( self, message: Union[list[PartUnionDict], PartUnionDict], - config: Optional[GenerateContentConfigOrDict] = None, + config: Optional[Union[GenerateContentConfigOrDict, ChatConfig]] = None, ) -> GenerateContentResponse: """Sends the conversation history with the additional message and returns model's response. @@ -424,6 +476,17 @@ async def send_message( ) input_content = t.t_content(message) method_config = config if config else self._config + + # tmp workaround before the next major version update + # extract afc config from ChatConfig and merge it into GenerateContentConfig + if isinstance(method_config, ChatConfig): + afc_config = method_config.automatic_function_calling_config + if afc_config.enable is not None: + afc_config.disable = not afc_config.enable + gc_config = _extract_generate_content_config(method_config) + gc_config.automatic_function_calling = afc_config + method_config = gc_config + method_config = _extra_utils.get_usage_header( method_config, types.GenerateContentConfig, usage="chat" # type: ignore[arg-type] ) @@ -453,7 +516,7 @@ async def send_message( async def send_message_stream( self, message: Union[list[PartUnionDict], PartUnionDict], - config: Optional[GenerateContentConfigOrDict] = None, + config: Optional[Union[GenerateContentConfigOrDict, ChatConfig]] = None, ) -> AsyncIterator[GenerateContentResponse]: """Sends the conversation history with the additional message and yields the model's response in chunks. @@ -482,6 +545,17 @@ async def send_message_stream( input_content = t.t_content(message) method_config = config if config else self._config + + # tmp workaround before the next major version update + # extract afc config from ChatConfig and merge it into GenerateContentConfig + if isinstance(method_config, ChatConfig): + afc_config = method_config.automatic_function_calling_config + if afc_config.enable is not None: + afc_config.disable = not afc_config.enable + gc_config = _extract_generate_content_config(method_config) + gc_config.automatic_function_calling = afc_config + method_config = gc_config + method_config = _extra_utils.get_usage_header( method_config, types.GenerateContentConfig, usage="chat" # type: ignore[arg-type] ) @@ -529,7 +603,7 @@ def create( self, *, model: str, - config: Optional[GenerateContentConfigOrDict] = None, + config: Optional[Union[GenerateContentConfigOrDict, ChatConfig]] = None, history: Optional[list[ContentOrDict]] = None, ) -> AsyncChat: """Creates a new chat session. diff --git a/google/genai/models.py b/google/genai/models.py index d77346acf..13d11c5ae 100644 --- a/google/genai/models.py +++ b/google/genai/models.py @@ -6554,6 +6554,14 @@ def generate_content( raise errors.UnsupportedFunctionError( 'MCP sessions are not supported in synchronous methods.' ) + if ( + parsed_config + and parsed_config.automatic_function_calling + and parsed_config.automatic_function_calling.enable is not None + ): + parsed_config.automatic_function_calling.disable = ( + not parsed_config.automatic_function_calling.enable + ) if _extra_utils.should_disable_afc(parsed_config): return self._generate_content( model=model, contents=contents, config=parsed_config @@ -6725,6 +6733,14 @@ def generate_content_stream( raise errors.UnsupportedFunctionError( 'MCP sessions are not supported in synchronous methods.' ) + if ( + parsed_config + and parsed_config.automatic_function_calling + and parsed_config.automatic_function_calling.enable is not None + ): + parsed_config.automatic_function_calling.disable = ( + not parsed_config.automatic_function_calling.enable + ) if _extra_utils.should_disable_afc(parsed_config): yield from self._generate_content_stream( model=model, contents=contents, config=parsed_config @@ -8762,6 +8778,14 @@ async def generate_content( ) ) + if ( + final_parsed_config + and final_parsed_config.automatic_function_calling + and final_parsed_config.automatic_function_calling.enable is not None + ): + final_parsed_config.automatic_function_calling.disable = ( + not final_parsed_config.automatic_function_calling.enable + ) if _extra_utils.should_disable_afc(final_parsed_config): return await self._generate_content( model=model, contents=contents, config=final_parsed_config @@ -9001,6 +9025,16 @@ async def stream_generator(): # type: ignore[no-untyped-def] ) ) + if ( + final_parsed_config + and final_parsed_config.automatic_function_calling + and final_parsed_config.automatic_function_calling.enable + is not None + ): + final_parsed_config.automatic_function_calling.disable = ( + not final_parsed_config.automatic_function_calling.enable + ) + if _extra_utils.should_disable_afc(final_parsed_config): response = await self._generate_content_stream( model=model, contents=contents, config=final_parsed_config diff --git a/google/genai/tests/chats/test_send_message.py b/google/genai/tests/chats/test_send_message.py index 0ec0f90b3..50e6026c0 100644 --- a/google/genai/tests/chats/test_send_message.py +++ b/google/genai/tests/chats/test_send_message.py @@ -945,3 +945,30 @@ async def test_async_server_side_mcp_tools(client): await chat.send_message( 'What is the weather in San Francisco on 02/02/2026?' ) + + +def test_afc_chat_config(client): + + def get_weather(location: str) -> str: + return f'The weather in {location} is sunny and 70 degrees.' + + chat = client.chats.create( + model='gemini-3.1-pro-preview', + config=types.ChatConfig( + tools=[get_weather], + automatic_function_calling_config=types.AutomaticFunctionCallingConfig( + enable=True + ), + ), + ) + response = chat.send_message('What is the weather in Boston?') + history = chat.get_history() + assert len(history) == 4 + assert history[0].role == 'user' + assert history[1].role == 'model' + assert history[1].parts[0].function_call.name == 'get_weather' + assert history[1].parts[0].function_call.args == {'location': 'Boston'} + assert history[2].role == 'user' + assert history[2].parts[0].function_response.name == 'get_weather' + assert history[3].role == 'model' + assert '70' in history[3].parts[0].text diff --git a/google/genai/types.py b/google/genai/types.py index 359c00d45..4ece8b5ec 100644 --- a/google/genai/types.py +++ b/google/genai/types.py @@ -5592,6 +5592,15 @@ class AutomaticFunctionCallingConfig(_common.BaseModel): GenerateContentResponse.automatic_function_calling_history. """, ) + enable: Optional[bool] = Field( + default=None, + description="""Whether to enable automatic function calling. + If not set or set to False, will not enable automatic function calling. + If set to True, will enable automatic function calling. + NOTE: This field takes precedence over the `disable` field. the `disable` + field will be deprecated. + """, + ) class AutomaticFunctionCallingConfigDict(TypedDict, total=False): @@ -5618,6 +5627,14 @@ class AutomaticFunctionCallingConfigDict(TypedDict, total=False): GenerateContentResponse.automatic_function_calling_history. """ + enable: Optional[bool] + """Whether to enable automatic function calling. + If not set or set to False, will not enable automatic function calling. + If set to True, will enable automatic function calling. + NOTE: This field takes precedence over the `disable` field. the `disable` + field will be deprecated. + """ + AutomaticFunctionCallingConfigOrDict = Union[ AutomaticFunctionCallingConfig, AutomaticFunctionCallingConfigDict @@ -22534,6 +22551,22 @@ class EmbedContentParametersDict(TypedDict, total=False): ] +class ChatConfig(GenerateContentConfig): + """Configuration for chat. + + This is a sub class of `GenerateContentConfig`, it supports all the + configurations in `GenerateContentConfig`. + """ + + automatic_function_calling_config: Optional[ + AutomaticFunctionCallingConfig + ] = Field( + default=None, + description="""The configuration for automatic function calling. + """, + ) + + class UserContent(Content): """UserContent facilitates the creation of a Content object with a user role.