diff --git a/google/genai/_gaos/resources/interactions/stepdelta/__init__.py b/google/genai/_gaos/resources/interactions/stepdelta/__init__.py index e45245eb5..831824b5c 100644 --- a/google/genai/_gaos/resources/interactions/stepdelta/__init__.py +++ b/google/genai/_gaos/resources/interactions/stepdelta/__init__.py @@ -62,6 +62,12 @@ from ....types.interactions.thoughtsummarydelta import ( ThoughtSummaryDelta as ThoughtSummary, ) +from ....types.interactions.toolsearchcalldelta import ( + ToolSearchCallDelta as ToolSearchCall, +) +from ....types.interactions.toolsearchresultdelta import ( + ToolSearchResultDelta as ToolSearchResult, +) from ....types.interactions.urlcontextcalldelta import ( URLContextCallDelta as URLContextCall, ) @@ -91,6 +97,8 @@ "TextAnnotationDelta", "ThoughtSignature", "ThoughtSummary", + "ToolSearchCall", + "ToolSearchResult", "URLContextCall", "URLContextResult", "Video", diff --git a/google/genai/_gaos/resources/interactions/tool/__init__.py b/google/genai/_gaos/resources/interactions/tool/__init__.py index 4f63f2c5c..fd9decc4c 100644 --- a/google/genai/_gaos/resources/interactions/tool/__init__.py +++ b/google/genai/_gaos/resources/interactions/tool/__init__.py @@ -23,6 +23,7 @@ from ....types.interactions.googlesearch import GoogleSearch from ....types.interactions.mcpserver import MCPServer from ....types.interactions.retrieval import Retrieval +from ....types.interactions.toolsearch import ToolSearch from ....types.interactions.urlcontext import URLContext from . import retrieval @@ -34,6 +35,7 @@ "GoogleSearch", "MCPServer", "Retrieval", + "ToolSearch", "URLContext", "retrieval", ] diff --git a/google/genai/_gaos/types/interactions/__init__.py b/google/genai/_gaos/types/interactions/__init__.py index 71bfd6481..8500420d8 100644 --- a/google/genai/_gaos/types/interactions/__init__.py +++ b/google/genai/_gaos/types/interactions/__init__.py @@ -365,6 +365,16 @@ from .tool import Tool, ToolParam, UnknownTool from .toolchoiceconfig import ToolChoiceConfig, ToolChoiceConfigParam from .toolchoicetype import ToolChoiceType + from .toolsearch import Execution, ToolSearch, ToolSearchParam + from .toolsearchcalldelta import ToolSearchCallDelta, ToolSearchCallDeltaTypedDict + from .toolsearchcallsteparguments import ( + ToolSearchCallStepArguments, + ToolSearchCallStepArgumentsTypedDict, + ) + from .toolsearchresultdelta import ( + ToolSearchResultDelta, + ToolSearchResultDeltaTypedDict, + ) from .transcriptionconfig import TranscriptionConfig, TranscriptionConfigParam from .urlcitation import URLCitation, URLCitationParam from .urlcontext import URLContext, URLContextParam @@ -488,6 +498,7 @@ "ErrorTypedDict", "ExaAISearchConfig", "ExaAISearchConfigParam", + "Execution", "FileCitation", "FileCitationParam", "FileContent", @@ -730,6 +741,14 @@ "ToolChoiceParam", "ToolChoiceType", "ToolParam", + "ToolSearch", + "ToolSearchCallDelta", + "ToolSearchCallDeltaTypedDict", + "ToolSearchCallStepArguments", + "ToolSearchCallStepArgumentsTypedDict", + "ToolSearchParam", + "ToolSearchResultDelta", + "ToolSearchResultDeltaTypedDict", "TranscriptionConfig", "TranscriptionConfigParam", "Transform", @@ -1115,6 +1134,15 @@ "ToolChoiceConfig": ".toolchoiceconfig", "ToolChoiceConfigParam": ".toolchoiceconfig", "ToolChoiceType": ".toolchoicetype", + "Execution": ".toolsearch", + "ToolSearch": ".toolsearch", + "ToolSearchParam": ".toolsearch", + "ToolSearchCallDelta": ".toolsearchcalldelta", + "ToolSearchCallDeltaTypedDict": ".toolsearchcalldelta", + "ToolSearchCallStepArguments": ".toolsearchcallsteparguments", + "ToolSearchCallStepArgumentsTypedDict": ".toolsearchcallsteparguments", + "ToolSearchResultDelta": ".toolsearchresultdelta", + "ToolSearchResultDeltaTypedDict": ".toolsearchresultdelta", "TranscriptionConfig": ".transcriptionconfig", "TranscriptionConfigParam": ".transcriptionconfig", "URLCitation": ".urlcitation", diff --git a/google/genai/_gaos/types/interactions/function.py b/google/genai/_gaos/types/interactions/function.py index 83822c568..2239261f0 100644 --- a/google/genai/_gaos/types/interactions/function.py +++ b/google/genai/_gaos/types/interactions/function.py @@ -29,18 +29,27 @@ class FunctionParam(TypedDict): r"""A tool that can be used by the model.""" + defer_loading: NotRequired[bool] + r"""If true, the function's loading is deferred.""" description: NotRequired[str] r"""A description of the function.""" name: NotRequired[str] r"""The name of the function.""" parameters: NotRequired[Any] r"""The JSON Schema for the function's parameters.""" + short_description: NotRequired[str] + r"""A brief description of the function, shown to the model as a + short summary of functions with `defer_loading` set to true. + """ type: Literal["function"] class Function(BaseModel): r"""A tool that can be used by the model.""" + defer_loading: Optional[bool] = None + r"""If true, the function's loading is deferred.""" + description: Optional[str] = None r"""A description of the function.""" @@ -50,6 +59,11 @@ class Function(BaseModel): parameters: Optional[Any] = None r"""The JSON Schema for the function's parameters.""" + short_description: Optional[str] = None + r"""A brief description of the function, shown to the model as a + short summary of functions with `defer_loading` set to true. + """ + type: Annotated[ Annotated[Literal["function"], AfterValidator(validate_const("function"))], pydantic.Field(alias="type"), @@ -57,7 +71,9 @@ class Function(BaseModel): @model_serializer(mode="wrap") def serialize_model(self, handler): - optional_fields = set(["description", "name", "parameters"]) + optional_fields = set( + ["defer_loading", "description", "name", "parameters", "short_description"] + ) serialized = handler(self) m = {} diff --git a/google/genai/_gaos/types/interactions/mcpserver.py b/google/genai/_gaos/types/interactions/mcpserver.py index 446ca783d..c64adc6da 100644 --- a/google/genai/_gaos/types/interactions/mcpserver.py +++ b/google/genai/_gaos/types/interactions/mcpserver.py @@ -32,6 +32,8 @@ class MCPServerParam(TypedDict): allowed_tools: NotRequired[List[AllowedToolsParam]] r"""The allowed tools.""" + defer_loading: NotRequired[bool] + r"""If true, loading of tools on this MCP server is deferred.""" headers: NotRequired[Dict[str, str]] r"""Optional: Fields for authentication headers, timeouts, etc., if needed.""" name: NotRequired[str] @@ -49,6 +51,9 @@ class MCPServer(BaseModel): allowed_tools: Optional[List[AllowedTools]] = None r"""The allowed tools.""" + defer_loading: Optional[bool] = None + r"""If true, loading of tools on this MCP server is deferred.""" + headers: Optional[Dict[str, str]] = None r"""Optional: Fields for authentication headers, timeouts, etc., if needed.""" @@ -67,7 +72,9 @@ class MCPServer(BaseModel): @model_serializer(mode="wrap") def serialize_model(self, handler): - optional_fields = set(["allowed_tools", "headers", "name", "url"]) + optional_fields = set( + ["allowed_tools", "defer_loading", "headers", "name", "url"] + ) serialized = handler(self) m = {} diff --git a/google/genai/_gaos/types/interactions/stepdeltadata.py b/google/genai/_gaos/types/interactions/stepdeltadata.py index b387efaa4..78be4e403 100644 --- a/google/genai/_gaos/types/interactions/stepdeltadata.py +++ b/google/genai/_gaos/types/interactions/stepdeltadata.py @@ -53,6 +53,8 @@ from .textdelta import TextDelta, TextDeltaTypedDict from .thoughtsignaturedelta import ThoughtSignatureDelta, ThoughtSignatureDeltaTypedDict from .thoughtsummarydelta import ThoughtSummaryDelta, ThoughtSummaryDeltaTypedDict +from .toolsearchcalldelta import ToolSearchCallDelta, ToolSearchCallDeltaTypedDict +from .toolsearchresultdelta import ToolSearchResultDelta, ToolSearchResultDeltaTypedDict from .urlcontextcalldelta import URLContextCallDelta, URLContextCallDeltaTypedDict from .urlcontextresultdelta import URLContextResultDelta, URLContextResultDeltaTypedDict from .videodelta import VideoDelta, VideoDeltaTypedDict @@ -80,13 +82,15 @@ GoogleMapsResultDeltaTypedDict, GoogleSearchCallDeltaTypedDict, URLContextCallDeltaTypedDict, + ToolSearchResultDeltaTypedDict, + ToolSearchCallDeltaTypedDict, CodeExecutionCallDeltaTypedDict, GoogleSearchResultDeltaTypedDict, - MCPServerToolResultDeltaTypedDict, RetrievalCallDeltaTypedDict, - MCPServerToolCallDeltaTypedDict, DocumentDeltaTypedDict, CodeExecutionResultDeltaTypedDict, + MCPServerToolResultDeltaTypedDict, + MCPServerToolCallDeltaTypedDict, URLContextResultDeltaTypedDict, ImageDeltaTypedDict, FunctionResultDeltaTypedDict, @@ -128,6 +132,8 @@ class UnknownStepDeltaData(BaseModel): "text": TextDelta, "thought_signature": ThoughtSignatureDelta, "thought_summary": ThoughtSummaryDelta, + "tool_search_call": ToolSearchCallDelta, + "tool_search_result": ToolSearchResultDelta, "url_context_call": URLContextCallDelta, "url_context_result": URLContextResultDelta, "video": VideoDelta, @@ -157,6 +163,8 @@ class UnknownStepDeltaData(BaseModel): TextDelta, ThoughtSignatureDelta, ThoughtSummaryDelta, + ToolSearchCallDelta, + ToolSearchResultDelta, URLContextCallDelta, URLContextResultDelta, VideoDelta, diff --git a/google/genai/_gaos/types/interactions/tool.py b/google/genai/_gaos/types/interactions/tool.py index c0f6e981b..dc1185d0b 100644 --- a/google/genai/_gaos/types/interactions/tool.py +++ b/google/genai/_gaos/types/interactions/tool.py @@ -25,6 +25,7 @@ from .googlesearch import GoogleSearch, GoogleSearchParam from .mcpserver import MCPServer, MCPServerParam from .retrieval import Retrieval, RetrievalParam +from .toolsearch import ToolSearch, ToolSearchParam from .urlcontext import URLContext, URLContextParam from functools import partial from .. import BaseModel @@ -42,9 +43,10 @@ URLContextParam, GoogleSearchParam, FileSearchParam, - FunctionParam, GoogleMapsParam, ComputerUseParam, + ToolSearchParam, + FunctionParam, MCPServerParam, RetrievalParam, ], @@ -71,6 +73,7 @@ class UnknownTool(BaseModel): "google_search": GoogleSearch, "mcp_server": MCPServer, "retrieval": Retrieval, + "tool_search": ToolSearch, "url_context": URLContext, } @@ -85,6 +88,7 @@ class UnknownTool(BaseModel): GoogleSearch, MCPServer, Retrieval, + ToolSearch, URLContext, UnknownTool, ], diff --git a/google/genai/_gaos/types/interactions/toolsearch.py b/google/genai/_gaos/types/interactions/toolsearch.py new file mode 100644 index 000000000..68afeaf7a --- /dev/null +++ b/google/genai/_gaos/types/interactions/toolsearch.py @@ -0,0 +1,105 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# pyformat: disable + +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from .. import BaseModel, UNSET_SENTINEL, UnrecognizedStr +from ...utils import validate_const +import pydantic +from pydantic import model_serializer +from pydantic.functional_validators import AfterValidator +from typing import Any, Literal, Optional, Union +from typing_extensions import Annotated, NotRequired, TypedDict + + +Execution = Union[ + Literal[ + "server", + "client", + ], + UnrecognizedStr, +] +r"""The execution mode of the tool search.""" + + +class ToolSearchParam(TypedDict): + r"""A tool that allows the model to dynamically search for and load tools into + the model’s context on demand. This allows clients to avoid loading all tool + definitions up front and may help reduce overall token usage and cost. + """ + + description: NotRequired[str] + r"""A description of the function. To be set only for client-side execution.""" + execution: NotRequired[Execution] + r"""The execution mode of the tool search.""" + name: NotRequired[str] + r"""The name of the function. To be set only for client-side execution.""" + parameters: NotRequired[Any] + r"""The JSON Schema for the function's parameters. To be set only for + client-side execution. + """ + type: Literal["tool_search"] + + +class ToolSearch(BaseModel): + r"""A tool that allows the model to dynamically search for and load tools into + the model’s context on demand. This allows clients to avoid loading all tool + definitions up front and may help reduce overall token usage and cost. + """ + + description: Optional[str] = None + r"""A description of the function. To be set only for client-side execution.""" + + execution: Optional[Execution] = None + r"""The execution mode of the tool search.""" + + name: Optional[str] = None + r"""The name of the function. To be set only for client-side execution.""" + + parameters: Optional[Any] = None + r"""The JSON Schema for the function's parameters. To be set only for + client-side execution. + """ + + type: Annotated[ + Annotated[ + Literal["tool_search"], AfterValidator(validate_const("tool_search")) + ], + pydantic.Field(alias="type"), + ] = "tool_search" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["description", "execution", "name", "parameters"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m + + +try: + ToolSearch.model_rebuild() +except NameError: + pass diff --git a/google/genai/_gaos/types/interactions/toolsearchcalldelta.py b/google/genai/_gaos/types/interactions/toolsearchcalldelta.py new file mode 100644 index 000000000..3304716b1 --- /dev/null +++ b/google/genai/_gaos/types/interactions/toolsearchcalldelta.py @@ -0,0 +1,74 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# pyformat: disable + +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from .toolsearchcallsteparguments import ( + ToolSearchCallStepArguments, + ToolSearchCallStepArgumentsTypedDict, +) +from .. import BaseModel, UNSET_SENTINEL +from ...utils import validate_const +import pydantic +from pydantic import model_serializer +from pydantic.functional_validators import AfterValidator +from typing import Literal, Optional +from typing_extensions import Annotated, NotRequired, TypedDict + + +class ToolSearchCallDeltaTypedDict(TypedDict): + arguments: NotRequired[ToolSearchCallStepArgumentsTypedDict] + signature: NotRequired[str] + r"""A signature hash for backend validation.""" + type: Literal["tool_search_call"] + + +class ToolSearchCallDelta(BaseModel): + arguments: Optional[ToolSearchCallStepArguments] = None + + signature: Optional[str] = None + r"""A signature hash for backend validation.""" + + type: Annotated[ + Annotated[ + Literal["tool_search_call"], + AfterValidator(validate_const("tool_search_call")), + ], + pydantic.Field(alias="type"), + ] = "tool_search_call" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["arguments", "signature"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m + + +try: + ToolSearchCallDelta.model_rebuild() +except NameError: + pass diff --git a/google/genai/_gaos/types/interactions/toolsearchcallsteparguments.py b/google/genai/_gaos/types/interactions/toolsearchcallsteparguments.py new file mode 100644 index 000000000..a812a11bc --- /dev/null +++ b/google/genai/_gaos/types/interactions/toolsearchcallsteparguments.py @@ -0,0 +1,49 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# pyformat: disable + +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from .. import BaseModel, UNSET_SENTINEL +from pydantic import model_serializer +from typing import List, Optional +from typing_extensions import NotRequired, TypedDict + + +class ToolSearchCallStepArgumentsTypedDict(TypedDict): + function_names: NotRequired[List[str]] + r"""List of function names to load explicitly.""" + + +class ToolSearchCallStepArguments(BaseModel): + function_names: Optional[List[str]] = None + r"""List of function names to load explicitly.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["function_names"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m diff --git a/google/genai/_gaos/types/interactions/toolsearchresultdelta.py b/google/genai/_gaos/types/interactions/toolsearchresultdelta.py new file mode 100644 index 000000000..65e8b86f7 --- /dev/null +++ b/google/genai/_gaos/types/interactions/toolsearchresultdelta.py @@ -0,0 +1,71 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# pyformat: disable + +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from .function import Function, FunctionParam +from .. import BaseModel, UNSET_SENTINEL +from ...utils import validate_const +import pydantic +from pydantic import model_serializer +from pydantic.functional_validators import AfterValidator +from typing import List, Literal, Optional +from typing_extensions import Annotated, NotRequired, TypedDict + + +class ToolSearchResultDeltaTypedDict(TypedDict): + result: NotRequired[List[FunctionParam]] + signature: NotRequired[str] + r"""A signature hash for backend validation.""" + type: Literal["tool_search_result"] + + +class ToolSearchResultDelta(BaseModel): + result: Optional[List[Function]] = None + + signature: Optional[str] = None + r"""A signature hash for backend validation.""" + + type: Annotated[ + Annotated[ + Literal["tool_search_result"], + AfterValidator(validate_const("tool_search_result")), + ], + pydantic.Field(alias="type"), + ] = "tool_search_result" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["result", "signature"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m + + +try: + ToolSearchResultDelta.model_rebuild() +except NameError: + pass diff --git a/google/genai/tests/interactions/test_tool_search.py b/google/genai/tests/interactions/test_tool_search.py new file mode 100644 index 000000000..1db79928a --- /dev/null +++ b/google/genai/tests/interactions/test_tool_search.py @@ -0,0 +1,244 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for Tool Search and defer_loading in Interactions API.""" + +import json +from unittest import mock +import pytest +from httpx import Request, Response +from httpx import Client as HTTPClient +from ... import Client +from ..._api_client import AsyncHttpxClient +from ..._gaos.types.interactions.stepdelta import StepDelta +from ..._gaos.types.interactions.toolsearchcalldelta import ToolSearchCallDelta +from ..._gaos.types.interactions.toolsearchresultdelta import ToolSearchResultDelta +from ...interactions import ( + Function, + MCPServer, + ToolSearch, +) + + +@pytest.fixture(autouse=True) +def set_env_vars(monkeypatch): + monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key") + monkeypatch.delenv("GOOGLE_CLOUD_PROJECT", raising=False) + monkeypatch.delenv("GOOGLE_CLOUD_LOCATION", raising=False) + + +def test_create_interaction_with_tool_search_and_mcp_defer_loading(): + client = Client() + + with mock.patch.object(HTTPClient, "send") as mock_send: + mock_send.return_value = Response( + 200, + request=Request("POST", ""), + headers={"content-type": "application/json"}, + content=json.dumps({"id": "interactions/test-id", "status": "completed"}), + ) + client.interactions.create( + model="gemini-2.5-flash", + input="What is the weather in Boston?", + tools=[ + ToolSearch(), + MCPServer( + name="weather_server", + url="https://example.com/mcp", + defer_loading=True, + ), + ], + ) + mock_send.assert_called_once() + request = mock_send.call_args[0][0] + body = json.loads(request.content.decode("utf-8")) + assert body["model"] == "gemini-2.5-flash" + assert body["tools"] == [ + {"type": "tool_search"}, + { + "type": "mcp_server", + "name": "weather_server", + "url": "https://example.com/mcp", + "defer_loading": True, + }, + ] + + +def test_create_interaction_with_tool_search_and_function_defer_loading(): + client = Client() + + with mock.patch.object(HTTPClient, "send") as mock_send: + mock_send.return_value = Response( + 200, + request=Request("POST", ""), + headers={"content-type": "application/json"}, + content=json.dumps({"id": "interactions/test-id", "status": "completed"}), + ) + client.interactions.create( + model="gemini-2.5-flash", + input="Find the stock price of GOOG", + tools=[ + {"type": "tool_search"}, + { + "type": "function", + "name": "get_stock_price", + "description": "Retrieves real-time stock price.", + "defer_loading": True, + "parameters": { + "type": "object", + "properties": {"ticker": {"type": "string"}}, + "required": ["ticker"], + }, + }, + ], + ) + mock_send.assert_called_once() + request = mock_send.call_args[0][0] + body = json.loads(request.content.decode("utf-8")) + assert body["tools"] == [ + {"type": "tool_search"}, + { + "type": "function", + "name": "get_stock_price", + "description": "Retrieves real-time stock price.", + "defer_loading": True, + "parameters": { + "type": "object", + "properties": {"ticker": {"type": "string"}}, + "required": ["ticker"], + }, + }, + ] + + +@pytest.mark.asyncio +async def test_async_create_interaction_with_tool_search(): + client = Client() + + with mock.patch.object(AsyncHttpxClient, "send") as mock_send: + mock_send.return_value = Response( + 200, + request=Request("POST", ""), + headers={"content-type": "application/json"}, + content=json.dumps({"id": "interactions/test-id", "status": "completed"}), + ) + await client.aio.interactions.create( + model="gemini-2.5-flash", + input="What is the weather?", + tools=[ + ToolSearch(), + MCPServer( + name="weather_server", + url="https://example.com/mcp", + defer_loading=True, + ), + ], + ) + mock_send.assert_called_once() + request = mock_send.call_args[0][0] + body = json.loads(request.content.decode("utf-8")) + assert body["tools"][0]["type"] == "tool_search" + assert body["tools"][1]["defer_loading"] is True + + +def test_deserialize_tool_search_call_and_result_deltas(): + call_delta_raw = { + "type": "tool_search_call", + "arguments": { + "function_names": ["weather_server_get_weather"], + "query": "weather in Boston", + }, + "signature": "sig_abc123", + } + step_delta = StepDelta.model_validate({"delta": call_delta_raw, "index": 0}) + assert isinstance(step_delta.delta, ToolSearchCallDelta) + assert step_delta.delta.type == "tool_search_call" + assert step_delta.delta.signature == "sig_abc123" + assert step_delta.delta.arguments.function_names == [ + "weather_server_get_weather" + ] + assert step_delta.delta.arguments.query == "weather in Boston" + + result_delta_raw = { + "type": "tool_search_result", + "result": [ + { + "name": "weather_server_get_weather", + "description": "Get current weather for location", + "defer_loading": False, + "parameters": { + "type": "object", + "properties": {"location": {"type": "string"}}, + }, + } + ], + "signature": "sig_xyz789", + } + step_delta_result = StepDelta.model_validate( + {"delta": result_delta_raw, "index": 1} + ) + assert isinstance(step_delta_result.delta, ToolSearchResultDelta) + assert step_delta_result.delta.type == "tool_search_result" + assert step_delta_result.delta.signature == "sig_xyz789" + assert len(step_delta_result.delta.result) == 1 + func = step_delta_result.delta.result[0] + assert isinstance(func, Function) + assert func.name == "weather_server_get_weather" + assert func.description == "Get current weather for location" + assert func.defer_loading is False + + +def test_create_interaction_with_client_tool_search(): + client = Client() + + with mock.patch.object(HTTPClient, "send") as mock_send: + mock_send.return_value = Response( + 200, + request=Request("POST", ""), + headers={"content-type": "application/json"}, + content=json.dumps({"id": "interactions/test-id", "status": "completed"}), + ) + client.interactions.create( + model="gemini-2.5-flash", + input="Find tools and answer", + tools=[ + ToolSearch( + execution="client", + name="custom_tool_search", + description="Client-side tool search implementation", + parameters={ + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + ), + ], + ) + mock_send.assert_called_once() + request = mock_send.call_args[0][0] + body = json.loads(request.content.decode("utf-8")) + assert body["tools"] == [ + { + "type": "tool_search", + "execution": "client", + "name": "custom_tool_search", + "description": "Client-side tool search implementation", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + } + ] +