From a990e4a3d9c293da3ac02b3c8538bdddaae784a9 Mon Sep 17 00:00:00 2001 From: Aftabbs Date: Thu, 9 Jul 2026 19:21:52 +0530 Subject: [PATCH 1/2] feat(google_genai): add google_server_tools parameter to GoogleGenAIChatGenerator Adds `google_server_tools: list[dict[str, Any]] | None = None` to `GoogleGenAIChatGenerator.__init__`, enabling users to pass Google's built-in server-side tools (e.g. google_search, code_execution) directly to the Gemini API. These tools are merged with any Haystack Tool/Toolset at request time: each dict is converted to a `google.genai.types.Tool` via model_validate and appended to the tools list sent in `GenerateContentConfig`. Follows the same pattern as `anthropic_server_tools` in #3386. Closes # --- .../google_genai/chat/chat_generator.py | 25 +++++++-- .../google_genai/tests/test_chat_generator.py | 55 +++++++++++++++++++ 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/integrations/google_genai/src/haystack_integrations/components/generators/google_genai/chat/chat_generator.py b/integrations/google_genai/src/haystack_integrations/components/generators/google_genai/chat/chat_generator.py index 6e689ccf3a..b3b5d867d1 100644 --- a/integrations/google_genai/src/haystack_integrations/components/generators/google_genai/chat/chat_generator.py +++ b/integrations/google_genai/src/haystack_integrations/components/generators/google_genai/chat/chat_generator.py @@ -201,6 +201,7 @@ def __init__( safety_settings: list[dict[str, Any]] | None = None, streaming_callback: StreamingCallbackT | None = None, tools: ToolsType | None = None, + google_server_tools: list[dict[str, Any]] | None = None, timeout: float | None = None, max_retries: int | None = None, ) -> None: @@ -236,6 +237,10 @@ def __init__( :param streaming_callback: A callback function that is called when a new token is received from the stream. :param tools: A list of Tool and/or Toolset objects, or a single Toolset for which the model can prepare calls. Each tool should have a unique name. + :param google_server_tools: A list of Google server-side (built-in) tools passed directly to the API. + Use this for native Google tools such as `{"google_search": {}}` or `{"code_execution": {}}`. + Each entry must be a dict that matches the `google.genai.types.Tool` schema. + These are merged with any Haystack tools at request time. :param timeout: Timeout for Google GenAI client calls. If not set, it defaults to the default set by the Google GenAI client. @@ -263,6 +268,7 @@ def __init__( self._safety_settings = safety_settings or [] self._streaming_callback = streaming_callback self._tools = tools + self._google_server_tools = google_server_tools self._timeout = timeout self._max_retries = max_retries @@ -291,6 +297,7 @@ def to_dict(self) -> dict[str, Any]: safety_settings=self._safety_settings, streaming_callback=callback_name, tools=serialized_tools, + google_server_tools=self._google_server_tools, timeout=self._timeout, max_retries=self._max_retries, ) @@ -448,9 +455,12 @@ def run( if safety_settings: config_params["safety_settings"] = safety_settings - # Add tools if provided - if tools: - config_params["tools"] = _convert_tools_to_google_genai_format(tools) + # Merge Haystack tools and Google server-side built-in tools + all_tools = _convert_tools_to_google_genai_format(tools) if tools else [] + if self._google_server_tools: + all_tools = all_tools + [types.Tool.model_validate(t) for t in self._google_server_tools] + if all_tools: + config_params["tools"] = all_tools config = types.GenerateContentConfig(**config_params) if config_params else None @@ -559,9 +569,12 @@ async def run_async( if safety_settings: config_params["safety_settings"] = safety_settings - # Add tools if provided - if tools: - config_params["tools"] = _convert_tools_to_google_genai_format(tools) + # Merge Haystack tools and Google server-side built-in tools + all_tools = _convert_tools_to_google_genai_format(tools) if tools else [] + if self._google_server_tools: + all_tools = all_tools + [types.Tool.model_validate(t) for t in self._google_server_tools] + if all_tools: + config_params["tools"] = all_tools config = types.GenerateContentConfig(**config_params) if config_params else None diff --git a/integrations/google_genai/tests/test_chat_generator.py b/integrations/google_genai/tests/test_chat_generator.py index e2df977cae..25f74973a3 100644 --- a/integrations/google_genai/tests/test_chat_generator.py +++ b/integrations/google_genai/tests/test_chat_generator.py @@ -299,6 +299,33 @@ def test_serde_with_response_format(self, monkeypatch): assert restored._generation_kwargs["response_format"] == schema assert restored._generation_kwargs["temperature"] == 0.5 + def test_init_with_google_server_tools(self, monkeypatch): + monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") + server_tool = {"google_search": {}} + component = GoogleGenAIChatGenerator(google_server_tools=[server_tool]) + assert component._google_server_tools == [server_tool] + + def test_to_dict_with_google_server_tools(self, monkeypatch): + monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") + server_tool = {"google_search": {}} + component = GoogleGenAIChatGenerator(google_server_tools=[server_tool]) + data = component.to_dict() + assert data["init_parameters"]["google_server_tools"] == [server_tool] + + def test_from_dict_with_google_server_tools(self, monkeypatch): + monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") + server_tool = {"google_search": {}} + component = GoogleGenAIChatGenerator(google_server_tools=[server_tool]) + data = component.to_dict() + restored = GoogleGenAIChatGenerator.from_dict(data) + assert restored._google_server_tools == [server_tool] + + def test_to_dict_default_google_server_tools_is_none(self, monkeypatch): + monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") + component = GoogleGenAIChatGenerator() + data = component.to_dict() + assert data["init_parameters"]["google_server_tools"] is None + class TestGoogleGenAIChatGeneratorRun: def test_run_non_streaming(self, monkeypatch, mock_response): @@ -365,6 +392,34 @@ def test_run_with_tools_passes_tools_to_config(self, monkeypatch, mock_response, config = call_kwargs.kwargs.get("config") or call_kwargs[1].get("config") assert config.tools is not None + def test_run_with_google_server_tools(self, monkeypatch, mock_response): + monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") + server_tool = {"google_search": {}} + component = GoogleGenAIChatGenerator(google_server_tools=[server_tool]) + component._client.models.generate_content = Mock(return_value=mock_response) + + component.run([ChatMessage.from_user("Search for AI news")]) + + call_kwargs = component._client.models.generate_content.call_args + config = call_kwargs.kwargs.get("config") or call_kwargs[1].get("config") + assert config.tools is not None + assert len(config.tools) == 1 + + def test_run_merges_haystack_and_google_server_tools(self, monkeypatch, mock_response, tools): + monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") + server_tool = {"google_search": {}} + component = GoogleGenAIChatGenerator(google_server_tools=[server_tool]) + component._client.models.generate_content = Mock(return_value=mock_response) + + component.run([ChatMessage.from_user("Search for AI news")], tools=tools) + + call_kwargs = component._client.models.generate_content.call_args + config = call_kwargs.kwargs.get("config") or call_kwargs[1].get("config") + assert config.tools is not None + # _convert_tools_to_google_genai_format bundles all function declarations into one Tool object, + # plus one Tool object for google_search + assert len(config.tools) == 2 + def test_run_with_safety_settings(self, monkeypatch, mock_response): monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") safety = [{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}] From 3ddd4b5f31c462d3dee1b9b997c12d60722d20a3 Mon Sep 17 00:00:00 2001 From: Aftabbs Date: Fri, 10 Jul 2026 13:38:17 +0530 Subject: [PATCH 2/2] =?UTF-8?q?refactor(google=5Fgenai):=20address=20revie?= =?UTF-8?q?w=20=E2=80=94=20improve=20docstring=20and=20move=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Expand google_server_tools docstring: explain dict-to-Tool conversion, list all built-in tools (google_search, code_execution, url_context), add usage example and link to Google built-in tools docs - Move serde tests (test_init/to_dict/from_dict_with_google_server_tools) adjacent to mixed-toolset tests in TestGoogleGenAIChatGeneratorInitSerDe - Add live integration test test_live_run_with_google_server_tools to TestGoogleGenAIChatGeneratorInference --- .../google_genai/chat/chat_generator.py | 25 ++++++- .../google_genai/tests/test_chat_generator.py | 67 +++++++++++-------- 2 files changed, 62 insertions(+), 30 deletions(-) diff --git a/integrations/google_genai/src/haystack_integrations/components/generators/google_genai/chat/chat_generator.py b/integrations/google_genai/src/haystack_integrations/components/generators/google_genai/chat/chat_generator.py index b3b5d867d1..33ebbec54a 100644 --- a/integrations/google_genai/src/haystack_integrations/components/generators/google_genai/chat/chat_generator.py +++ b/integrations/google_genai/src/haystack_integrations/components/generators/google_genai/chat/chat_generator.py @@ -238,9 +238,28 @@ def __init__( :param tools: A list of Tool and/or Toolset objects, or a single Toolset for which the model can prepare calls. Each tool should have a unique name. :param google_server_tools: A list of Google server-side (built-in) tools passed directly to the API. - Use this for native Google tools such as `{"google_search": {}}` or `{"code_execution": {}}`. - Each entry must be a dict that matches the `google.genai.types.Tool` schema. - These are merged with any Haystack tools at request time. + Each entry must be a dict matching the ``google.genai.types.Tool`` schema. The dict keys correspond + to the built-in tool type and the values are their configuration dicts (often empty ``{}``). + These are merged with any Haystack ``tools`` at request time. + + Supported built-in tools: + + - ``{"google_search": {}}`` — enables real-time Google Search grounding + - ``{"code_execution": {}}`` — enables Python code execution in a sandbox + - ``{"url_context": {}}`` — enables fetching and using URL content in context + + Example:: + + from haystack.dataclasses import ChatMessage + from haystack_integrations.components.generators.google_genai import GoogleGenAIChatGenerator + + chat_generator = GoogleGenAIChatGenerator( + google_server_tools=[{"google_search": {}}, {"code_execution": {}}] + ) + response = chat_generator.run([ChatMessage.from_user("What happened in AI today?")]) + + For the full list of built-in tools and their parameters, see: + https://ai.google.dev/gemini-api/docs/tools#built-in-tools :param timeout: Timeout for Google GenAI client calls. If not set, it defaults to the default set by the Google GenAI client. diff --git a/integrations/google_genai/tests/test_chat_generator.py b/integrations/google_genai/tests/test_chat_generator.py index 25f74973a3..f1546e31ac 100644 --- a/integrations/google_genai/tests/test_chat_generator.py +++ b/integrations/google_genai/tests/test_chat_generator.py @@ -253,6 +253,33 @@ def test_serde_with_mixed_tools_and_toolsets(self, monkeypatch): assert restored._tools[0].name == "tool1" assert len(restored._tools[1]) == 1 + def test_init_with_google_server_tools(self, monkeypatch): + monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") + server_tool = {"google_search": {}} + component = GoogleGenAIChatGenerator(google_server_tools=[server_tool]) + assert component._google_server_tools == [server_tool] + + def test_to_dict_with_google_server_tools(self, monkeypatch): + monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") + server_tool = {"google_search": {}} + component = GoogleGenAIChatGenerator(google_server_tools=[server_tool]) + data = component.to_dict() + assert data["init_parameters"]["google_server_tools"] == [server_tool] + + def test_from_dict_with_google_server_tools(self, monkeypatch): + monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") + server_tool = {"google_search": {}} + component = GoogleGenAIChatGenerator(google_server_tools=[server_tool]) + data = component.to_dict() + restored = GoogleGenAIChatGenerator.from_dict(data) + assert restored._google_server_tools == [server_tool] + + def test_to_dict_default_google_server_tools_is_none(self, monkeypatch): + monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") + component = GoogleGenAIChatGenerator() + data = component.to_dict() + assert data["init_parameters"]["google_server_tools"] is None + def test_to_dict_with_response_format_pydantic(self, monkeypatch): """Test that to_dict serializes a Pydantic response_format to a JSON schema dict.""" monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") @@ -299,33 +326,6 @@ def test_serde_with_response_format(self, monkeypatch): assert restored._generation_kwargs["response_format"] == schema assert restored._generation_kwargs["temperature"] == 0.5 - def test_init_with_google_server_tools(self, monkeypatch): - monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") - server_tool = {"google_search": {}} - component = GoogleGenAIChatGenerator(google_server_tools=[server_tool]) - assert component._google_server_tools == [server_tool] - - def test_to_dict_with_google_server_tools(self, monkeypatch): - monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") - server_tool = {"google_search": {}} - component = GoogleGenAIChatGenerator(google_server_tools=[server_tool]) - data = component.to_dict() - assert data["init_parameters"]["google_server_tools"] == [server_tool] - - def test_from_dict_with_google_server_tools(self, monkeypatch): - monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") - server_tool = {"google_search": {}} - component = GoogleGenAIChatGenerator(google_server_tools=[server_tool]) - data = component.to_dict() - restored = GoogleGenAIChatGenerator.from_dict(data) - assert restored._google_server_tools == [server_tool] - - def test_to_dict_default_google_server_tools_is_none(self, monkeypatch): - monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") - component = GoogleGenAIChatGenerator() - data = component.to_dict() - assert data["init_parameters"]["google_server_tools"] is None - class TestGoogleGenAIChatGeneratorRun: def test_run_non_streaming(self, monkeypatch, mock_response): @@ -781,6 +781,19 @@ def test_live_run_with_parallel_tools(self, tools): # Check that the response mentions both temperature readings assert "22" in final_message.text and "15" in final_message.text + def test_live_run_with_google_server_tools(self): + """ + Integration test that google_server_tools (Google Search) is accepted by the API and produces a response. + """ + component = GoogleGenAIChatGenerator(google_server_tools=[{"google_search": {}}]) + results = component.run([ChatMessage.from_user("What is the capital of France?")]) + + assert len(results["replies"]) == 1 + message = results["replies"][0] + assert message.text + assert message.meta["model"] + assert message.meta["finish_reason"] == "stop" + def test_live_run_with_thinking(self): """ Integration test for the thinking feature with a model that supports it.